fix(clawhubadapter): manager CLI improvements, pysimulator updates, sidecar fixes

This commit is contained in:
root
2026-07-23 13:53:55 +08:00
parent 659545069a
commit 3a5f548326
5 changed files with 671 additions and 11 deletions

View File

@ -16,6 +16,79 @@ function readJSON(file) {
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { return null; }
}
// ---- Enhanced OC Plugin Detection ----
function resolvePackageEntry(dir, pkg) {
const candidates = [];
if (pkg) {
if (pkg.main) candidates.push(path.resolve(dir, pkg.main));
if (pkg.exports) {
const exp = pkg.exports;
if (typeof exp === 'string') candidates.push(path.resolve(dir, exp));
if (exp['.']) {
const dot = exp['.'];
if (typeof dot === 'string') candidates.push(path.resolve(dir, dot));
if (dot.require) candidates.push(path.resolve(dir, dot.require));
if (dot.default) candidates.push(path.resolve(dir, dot.default));
}
}
}
if (!pkg || !pkg.main) {
for (const name of ['index.js', 'main.js', 'src/index.js', 'lib/index.js']) {
candidates.push(path.join(dir, name));
}
}
for (const cp of candidates) {
if (fs.existsSync(cp)) return cp;
}
return null;
}
function ensureOCManifest(dir, name) {
const manifestPath = path.join(dir, 'openclaw.plugin.json');
if (fs.existsSync(manifestPath)) return;
const pkg = readJSON(path.join(dir, 'package.json'));
const entry = resolvePackageEntry(dir, pkg);
const relEntry = entry ? path.relative(dir, entry) : 'index.js';
const manifest = {
name: name,
version: (pkg && pkg.version) || '1.0.0',
entry: relEntry,
description: (pkg && pkg.description) || 'OpenClaw plugin (auto-detected)'
};
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
process.stderr.write(`[manager] created synthetic manifest: ${manifestPath}\n`);
}
function tryDetectOCPackage(pkgDir, pkgName) {
const pkg = readJSON(path.join(pkgDir, 'package.json'));
if (!pkg) return null;
const entryPath = resolvePackageEntry(pkgDir, pkg);
if (!entryPath) return null;
try {
delete require.cache[require.resolve(entryPath)];
const mod = require(entryPath);
const entry = mod.default || mod;
if (entry && typeof entry === 'object' && typeof entry.register === 'function') {
const name = pkgName || (pkg.openclaw ? (pkg.openclaw.name || pkg.name) : pkg.name) || path.basename(pkgDir);
ensureOCManifest(pkgDir, name);
process.stderr.write(`[manager] enhanced detection found OC plugin: ${name} (via require)\n`);
return { dir: pkgDir, name };
}
} catch (e) {
process.stderr.write(`[manager] require detect failed for ${pkgDir}: ${e.message}\n`);
}
return null;
}
// ---- Plugin registry ----
const loadedPlugins = {}; // name -> { entry, tools: [{name, execute, ...}] }
const allTools = []; // flat list of all tools across all plugins
@ -180,6 +253,27 @@ function installNPMPackage(spec, skillsDir) {
}
}
if (!foundPluginDir) {
process.stderr.write(`[manager] standard scan failed, trying enhanced detection...\n`);
for (const entry of entries) {
if (foundPluginDir) break;
const dir = path.join(nm, entry);
if (!fs.statSync(dir).isDirectory()) continue;
if (entry.startsWith('@')) {
for (const sub of fs.readdirSync(dir)) {
if (foundPluginDir) break;
const subDir = path.join(dir, sub);
const detected = tryDetectOCPackage(subDir, entry + '/' + sub);
if (detected) { foundPluginDir = detected.dir; foundName = detected.name; }
}
} else {
const detected = tryDetectOCPackage(dir, entry);
if (detected) { foundPluginDir = detected.dir; foundName = detected.name; }
}
}
}
if (!foundPluginDir) {
fs.rmSync(installDir, { recursive: true, force: true });
return { error: `no OC plugin found in installed package "${spec}"` };
@ -207,29 +301,195 @@ function cpSync(src, dst) {
}
}
// ---- OpenClaw CLI compatibility ----
function showQR(text) {
try {
const qrcode = require('qrcode');
qrcode.generate(text, { small: true }, (qr) => process.stdout.write(qr + '\n'));
} catch {
process.stdout.write(`QR: ${text}\n`);
process.stdout.write('(install qrcode package for QR display: npm install qrcode)\n');
}
}
async function runCLI(skillsDir, cliArgs) {
const cmd = cliArgs[0] || '';
switch (cmd) {
case 'plugin:install': {
const spec = cliArgs[1];
if (!spec) throw new Error('Usage: openclaw plugin:install <npm:package|clawhub:name|path>');
const result = installNPMPackage(spec, skillsDir);
if (result.error) throw new Error(result.error);
console.log(`Installed: ${result.name}`);
break;
}
case 'plugin:uninstall': {
const name = cliArgs[1];
if (!name) throw new Error('Usage: openclaw plugin:uninstall <name>');
const targetDir = path.join(skillsDir, name);
if (!fs.existsSync(targetDir)) throw new Error(`Plugin not found: ${name}`);
fs.rmSync(targetDir, { recursive: true, force: true });
console.log(`Uninstalled: ${name}`);
break;
}
case 'plugin:list': {
if (!fs.existsSync(skillsDir)) { console.log('(no plugins)'); break; }
let count = 0;
for (const entry of fs.readdirSync(skillsDir)) {
if (entry.startsWith('.')) continue;
const pluginDir = path.join(skillsDir, entry);
if (!fs.statSync(pluginDir).isDirectory()) continue;
const pkg = readJSON(path.join(pluginDir, 'package.json'));
const manifest = readJSON(path.join(pluginDir, 'openclaw.plugin.json'));
if (pkg || manifest) {
const version = pkg?.version || manifest?.version || '?';
const desc = pkg?.description || manifest?.description || '';
console.log(` ${entry} v${version}${desc ? ' — ' + desc : ''}`);
count++;
}
}
if (count === 0) console.log('(no OpenClaw plugins)');
break;
}
case 'auth:login':
case 'auth:qrcode': {
const url = cliArgs[1] || 'openclaw://auth';
console.log('Scan the QR code to log in:');
showQR(url);
console.log('\nOr open this URL:');
console.log(` ${url}`);
break;
}
case 'auth:status': {
console.log('Auth status: not implemented (running in HomeAgent mode)');
break;
}
case 'config:get': {
const key = cliArgs[1];
if (!key) throw new Error('Usage: openclaw config:get <key>');
// TODO: read from HomeAgent config system when bridged
console.log(`(not available in CLI mode: ${key})`);
break;
}
case 'config:set': {
const key = cliArgs[1];
const value = cliArgs[2];
if (!key || value === undefined) throw new Error('Usage: openclaw config:set <key> <value>');
// TODO: write to HomeAgent config system when bridged
console.log(`(not available in CLI mode: ${key}=${value})`);
break;
}
case 'config:list':
console.log('(not available in CLI mode)');
break;
case 'env': {
const info = {
homeAgent: true,
openclawVersion: 'compatible',
platform: process.platform,
nodeVersion: process.version,
skillsDir,
};
console.log(JSON.stringify(info, null, 2));
break;
}
case '--version':
case 'version':
console.log('HomeAgent OpenClaw Adapter 1.0.0 (openclaw-compatible)');
break;
case 'help':
case '--help':
console.log(`Usage: openclaw <command> [args]
Commands:
plugin:install <spec> Install a plugin (npm:xxx, clawhub:xxx, or path)
plugin:uninstall <name> Uninstall a plugin
plugin:list List installed plugins
auth:login [url] Show QR code for login/binding
auth:status Check authentication status
config:get <key> Get config value
config:set <key> <val> Set config value
config:list List all config
env Show runtime environment info
--version Show version
help Show this help`);
break;
default:
throw new Error(`Unknown command: ${cmd}\nRun 'openclaw help' for usage.`);
}
}
// ---- Main ----
const args = process.argv.slice(2);
if (args.length < 1) {
process.stderr.write('[manager] usage: node main.js <skills-dir>\n');
process.stderr.write('[manager] usage: node main.js <skills-dir> [openclaw-command...]\n');
process.exit(1);
}
const skillsDir = path.resolve(args[0]);
// CLI mode: if additional args provided, run as openclaw CLI command and exit
if (args.length > 1) {
const cliArgs = args.slice(1);
runCLI(skillsDir, cliArgs).then(() => process.exit(0)).catch(e => {
process.stderr.write(`Error: ${e.message}\n`);
process.exit(1);
});
return;
}
// Server mode: persist skills dir reference for CLI wrapper
try {
const simDir = path.dirname(process.argv[1]);
fs.writeFileSync(path.join(simDir, '.skillsdir'), skillsDir);
} catch (e) {
process.stderr.write(`[manager] warning: could not write .skillsdir: ${e.message}\n`);
}
// Add our bin directory to PATH so subprocesses can find 'openclaw' CLI
try {
const binDir = path.join(path.dirname(process.argv[1]), 'bin');
if (fs.existsSync(binDir)) {
const PATH = process.env.PATH || '';
if (!PATH.includes(binDir)) {
process.env.PATH = binDir + path.delimiter + PATH;
}
}
} catch (e) {
process.stderr.write(`[manager] warning: could not update PATH: ${e.message}\n`);
}
// Load existing plugins on startup
process.stderr.write(`[manager] scanning: ${skillsDir}\n`);
if (fs.existsSync(skillsDir)) {
for (const entry of fs.readdirSync(skillsDir)) {
if (entry.startsWith('.')) continue; // skip hidden
if (entry.startsWith('.')) continue;
const pluginDir = path.join(skillsDir, entry);
if (!fs.statSync(pluginDir).isDirectory()) continue;
if (fs.existsSync(path.join(pluginDir, 'main.js')) || fs.existsSync(path.join(pluginDir, 'main.py'))) {
process.stderr.write(`[manager] skip non-OC plugin: ${entry} (main.js/main.py)\n`);
continue;
}
if (fs.existsSync(path.join(pluginDir, 'openclaw.plugin.json')) ||
(fs.existsSync(path.join(pluginDir, 'package.json')) && readJSON(path.join(pluginDir, 'package.json'))?.openclaw)) {
loadPlugin(pluginDir, entry);
} else {
const detected = tryDetectOCPackage(pluginDir, entry);
if (detected) {
loadPlugin(detected.dir, detected.name);
}
}
}
}
@ -301,6 +561,66 @@ rl.on('line', async (line) => {
return;
}
if (method === 'plugins/detect') {
const pluginDir = req.params?.dir;
const name = req.params?.name || (pluginDir ? path.basename(pluginDir) : '');
if (!pluginDir) { sendError(id, -32602, 'dir required'); return; }
const resolvedDir = path.resolve(pluginDir);
if (!fs.existsSync(resolvedDir)) {
sendError(id, -32601, `directory not found: ${resolvedDir}`);
return;
}
if (loadedPlugins[name]) {
writeJSON({ jsonrpc: '2.0', id, result: { name, tools: loadedPlugins[name].tools.map(t => t.name), type: 'loaded' } });
return;
}
// Install npm dependencies if package.json exists with deps
const pkgPath = path.join(resolvedDir, 'package.json');
if (fs.existsSync(pkgPath)) {
const pkg = readJSON(pkgPath);
if (pkg) {
const hasDeps = (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) ||
(pkg.devDependencies && Object.keys(pkg.devDependencies).length > 0);
if (hasDeps) {
try {
execSync(`npm install --no-save --prefix "${resolvedDir}"`, {
cwd: resolvedDir, stdio: ['pipe', 'pipe', 'pipe'],
timeout: 120000, env: { ...process.env, NODE_PATH: path.join(resolvedDir, 'node_modules') }
});
process.stderr.write(`[manager] installed dependencies for ${name}\n`);
} catch (e) {
process.stderr.write(`[manager] npm install failed for ${name}: ${e.message}\n`);
}
}
}
}
let detected = null;
if (fs.existsSync(path.join(resolvedDir, 'openclaw.plugin.json')) ||
(fs.existsSync(path.join(resolvedDir, 'package.json')) && readJSON(path.join(resolvedDir, 'package.json'))?.openclaw)) {
detected = { dir: resolvedDir, name };
} else {
detected = tryDetectOCPackage(resolvedDir, name);
}
if (!detected) {
sendError(id, -32601, `no OC plugin detected in: ${resolvedDir}`);
return;
}
const ok = loadPlugin(detected.dir, detected.name);
if (!ok) {
sendError(id, -32603, `failed to load detected plugin: ${detected.name}`);
return;
}
writeJSON({ jsonrpc: '2.0', id, result: { name: detected.name, tools: loadedPlugins[detected.name].tools.map(t => t.name), type: detected === Object(detected) && detected.dir === resolvedDir ? 'detected' : 'standard' } });
return;
}
if (method === 'tools/list') {
const tools = allTools.map(t => ({
name: t.name,

View File

@ -0,0 +1,22 @@
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
const { fork } = require('child_process');
const binDir = __dirname;
const skillsdirPath = path.join(binDir, '..', '.skillsdir');
if (!fs.existsSync(skillsdirPath)) {
process.stderr.write('Error: OpenClaw manager not running (no .skillsdir found)\n');
process.exit(1);
}
const skillsDir = fs.readFileSync(skillsdirPath, 'utf8').trim();
const managerPath = path.join(binDir, '..', 'manager.js');
const proc = fork(managerPath, [skillsDir, ...process.argv.slice(2)], {
stdio: 'inherit',
env: { ...process.env, OPENCLAW_CLI: '1' },
});
proc.on('exit', (code) => process.exit(code));

View File

@ -30,6 +30,9 @@ var managerSrc string
//go:embed pysimulator/main.py
var pySimulatorSrc string
//go:embed manager/openclaw_cli.js
var openclawCliSrc string
var SkillsDir string
var SimulatorDir string
@ -217,6 +220,16 @@ func (p *Plugin) launchManager(s *sdk.PluginSDK) error {
return fmt.Errorf("write manager: %w", err)
}
// Write openclaw CLI wrapper so plugins can exec 'openclaw plugin:install' etc.
cliBinDir := filepath.Join(p.simulatorDir, "bin")
if err := os.MkdirAll(cliBinDir, 0755); err != nil {
return fmt.Errorf("create cli bin dir: %w", err)
}
openclawPath := filepath.Join(cliBinDir, "openclaw")
if err := os.WriteFile(openclawPath, []byte(openclawCliSrc), 0755); err != nil {
return fmt.Errorf("write openclaw CLI: %w", err)
}
// Ensure skills dir exists for the manager to scan
os.MkdirAll(p.skillsDir, 0755)
@ -323,6 +336,30 @@ func (p *Plugin) installFromClawHub(spec string) (interface{}, error) {
}
if err := p.reloadPlugin(name); err != nil {
// Fallback: try manager's enhanced detection
p.mu.Lock()
mgr := p.manager
p.mu.Unlock()
if mgr != nil {
data, mgrErr := mgr.call("plugins/detect", map[string]interface{}{
"dir": extractDir,
"name": name,
})
if mgrErr == nil {
var result struct {
Name string `json:"name"`
Tools []string `json:"tools"`
Type string `json:"type"`
}
if json.Unmarshal(data, &result) == nil {
return map[string]interface{}{
"content": fmt.Sprintf("已从 ClawHub 安装插件: %s\n 工具: %s", result.Name, strings.Join(result.Tools, ", ")),
}, nil
}
}
}
return errorResult(fmt.Sprintf("loaded but with warning: %v", err)), nil
}

View File

@ -14,11 +14,14 @@ Plugin main.py should define:
def register(api):
api.register_tool(name, description, parameters, execute_fn)
"""
import asyncio
import importlib.util
import inspect
import json
import os
import sys
def resolve_entry(plugin_dir):
"""Resolve the plugin entry point."""
main_py = os.path.join(plugin_dir, 'main.py')
@ -45,7 +48,6 @@ class PluginAPI:
def register_tool(self, name, description='', parameters=None, execute=None):
if parameters is None:
parameters = {'type': 'object', 'properties': {}}
# Notify Go side
self._notify('register', {
'type': 'tool',
'data': {
@ -79,8 +81,261 @@ class PluginAPI:
sys.stdout.flush()
# ---- Legacy Python Plugin Detection ----
def _resolve_env_vars(plugin_dir):
"""Read env var requirements from skill.json and resolve from OS env."""
skill_json = os.path.join(plugin_dir, 'skill.json')
if not os.path.isfile(skill_json):
return {}
try:
with open(skill_json, 'r') as f:
meta = json.load(f)
except Exception:
return {}
env_map = {}
for key in meta.get('required_env_vars', []):
val = os.environ.get(key) or os.environ.get(key.lower())
if val:
env_map[key.lower()] = val
for key in meta.get('optional_env_vars', []):
val = os.environ.get(key) or os.environ.get(key.lower())
if val:
env_map[key.lower()] = val
return env_map
def _collect_sub_skills(instance):
"""Discover sub-module names from a class instance by finding attributes with execute methods.
Prefer instance __dict__ attributes over properties to avoid triggering side effects.
"""
skills = []
seen = set()
# First look at instance __dict__ (safe, no property triggers)
for attr_name in list(instance.__dict__.keys()):
if attr_name.startswith('_'):
continue
attr = instance.__dict__[attr_name]
seen.add(attr_name)
if hasattr(attr, 'execute') and callable(attr.execute):
try:
sig = inspect.signature(attr.execute)
params = list(sig.parameters.keys())
if 'action' in params and len(params) >= 2:
skills.append(attr_name)
except (ValueError, TypeError):
continue
# Also check class-level non-property attributes
for attr_name in dir(type(instance)):
if attr_name.startswith('_') or attr_name in seen:
continue
try:
attr = getattr(type(instance), attr_name, None)
except Exception:
continue
if isinstance(attr, property):
continue # skip properties (may raise on access)
try:
attr = getattr(instance, attr_name, None)
except Exception:
continue
if attr is None:
continue
seen.add(attr_name)
if hasattr(attr, 'execute') and callable(attr.execute):
try:
sig = inspect.signature(attr.execute)
params = list(sig.parameters.keys())
if 'action' in params and len(params) >= 2:
skills.append(attr_name)
except (ValueError, TypeError):
continue
return skills
def _readable_class_name(cls):
"""Convert PascalCase to snake_case for readable names."""
import re
name = cls.__name__
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
def _make_async_execute(instance, skill_name):
"""Wrap an async sub-module execute into a sync callable for JSON-RPC."""
def execute(args):
action = args.pop('action', '') if isinstance(args, dict) else ''
result = asyncio.run(instance.execute(skill_name=skill_name, action=action, **args))
return result
return execute
def _make_legacy_execute(instance, attr_name):
"""Wrap an async legacy execute into a sync callable for JSON-RPC."""
def execute(args):
action = args.pop('action', '') if isinstance(args, dict) else ''
handler = getattr(instance, attr_name)
result = asyncio.run(handler.execute(action=action, **args))
return result
return execute
def detect_legacy_plugin(module, plugin_dir, plugin_name):
"""Auto-detect legacy Python plugin patterns and register as OC tools.
Returns a PluginAPI with discovered tools, or None if no pattern matches.
"""
api = PluginAPI(plugin_dir)
# Pattern 1: Module has a main class with execute(skill_name, action, **kwargs)
# Look for any class that has an 'execute' method accepting 'skill_name'
main_class = None
main_class_name = None
for name in dir(module):
obj = getattr(module, name, None)
if not isinstance(obj, type):
continue
if not hasattr(obj, 'execute') or not callable(getattr(obj, 'execute', None)):
continue
try:
sig = inspect.signature(obj.execute)
params = list(sig.parameters.keys())
if 'skill_name' in params:
main_class = obj
main_class_name = name
break
except (ValueError, TypeError):
continue
if main_class is None:
return None
# Resolve env vars for constructor
env_args = _resolve_env_vars(plugin_dir)
try:
sig = inspect.signature(main_class.__init__)
init_params = list(sig.parameters.keys())[1:] # skip self
kwargs = {}
for p in init_params:
lower_p = p.lower()
if lower_p in env_args:
kwargs[p] = env_args[lower_p]
sys.stderr.write(f'[pysimulator] legacy: instantiating {main_class_name} with {kwargs}\n')
instance = main_class(**kwargs)
except Exception as e:
sys.stderr.write(f'[pysimulator] legacy: cannot instantiate {main_class_name}: {e}\n')
return None
# Discover sub-modules
sub_skills = _collect_sub_skills(instance)
sys.stderr.write(f'[pysimulator] legacy: discovered sub-skills: {sub_skills}\n')
discovered_actions = {}
for attr_name in sub_skills:
handler = getattr(instance, attr_name)
# Build parameter schema from the execute signature
try:
sig = inspect.signature(handler.execute)
param_props = {}
for p_name, p_sig in list(sig.parameters.items())[1:]: # skip 'self'
if p_name == 'action':
param_props['action'] = {
'type': 'string',
'description': f'Action to perform in {attr_name}',
}
continue
if p_sig.annotation != inspect.Parameter.empty:
p_type = 'string'
if p_sig.annotation is int:
p_type = 'number'
elif p_sig.annotation is bool:
p_type = 'boolean'
elif p_sig.annotation is float:
p_type = 'number'
else:
anno_str = str(p_sig.annotation)
if 'int' in anno_str:
p_type = 'number'
elif 'bool' in anno_str:
p_type = 'boolean'
elif 'float' in anno_str:
p_type = 'number'
param_props[p_name] = {
'type': p_type,
'description': f'Parameter {p_name}',
}
else:
param_props[p_name] = {
'type': 'string',
'description': f'Parameter {p_name}',
}
required = ['action']
schema = {
'type': 'object',
'properties': param_props,
'required': required,
}
tool_name = attr_name
doc = handler.execute.__doc__ or f'{attr_name} operations'
first_line = doc.strip().split('\n')[0] if doc else f'{attr_name} module'
discovered_actions[attr_name] = (tool_name, first_line, schema)
except (ValueError, TypeError) as e:
sys.stderr.write(f'[pysimulator] legacy: skip {attr_name}: {e}\n')
continue
if discovered_actions:
# Register each sub-skill as a separate OC tool
for attr_name, (tool_name, desc, schema) in discovered_actions.items():
handler = getattr(instance, attr_name)
api.register_tool(
name=tool_name,
description=f'{plugin_name}: {desc}',
parameters=schema,
execute=_make_legacy_execute(instance, attr_name),
)
sys.stderr.write(f'[pysimulator] legacy registered tool: {tool_name}\n')
else:
# Fallback: single tool wrapping the whole plugin
try:
sig = inspect.signature(main_class.execute)
skill_params = [p for p in list(sig.parameters.keys())[1:]] # skip self
param_props = {}
for p in skill_params:
param_props[p] = {'type': 'string', 'description': p}
api.register_tool(
name=plugin_name,
description=f'{plugin_name}: unified operations',
parameters={
'type': 'object',
'properties': param_props,
'required': skill_params,
},
execute=_make_async_execute(instance, None),
)
sys.stderr.write(f'[pysimulator] legacy registered fallback tool: {plugin_name}\n')
except Exception as e:
sys.stderr.write(f'[pysimulator] legacy: fallback failed: {e}\n')
return None
return api
def load_plugin(plugin_dir):
"""Load the plugin from main.py and call its register()."""
"""Load the plugin from main.py.
Tries OC-style register(api) first, then auto-detects legacy patterns.
"""
entry_path = resolve_entry(plugin_dir)
if entry_path is None:
sys.stderr.write(f'[pysimulator] entry not found in {plugin_dir}\n')
@ -91,13 +346,22 @@ def load_plugin(plugin_dir):
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if not hasattr(module, 'register'):
sys.stderr.write(f'[pysimulator] {entry_path} must define a register(api) function\n')
sys.exit(1)
plugin_name = os.path.basename(plugin_dir)
api = PluginAPI(plugin_dir)
module.register(api)
return api
# OC-style: register(api)
if hasattr(module, 'register'):
api = PluginAPI(plugin_dir)
module.register(api)
return api
# Legacy auto-detection
sys.stderr.write(f'[pysimulator] no register() function, trying legacy detection for {plugin_name}...\n')
api = detect_legacy_plugin(module, plugin_dir, plugin_name)
if api is not None:
return api
sys.stderr.write(f'[pysimulator] {entry_path} must define a register(api) function\n')
sys.exit(1)
def main():

View File

@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
)
@ -162,6 +163,22 @@ func launchProcess(bin, arg, dir, name string) (*sidecarProcess, error) {
cmd.Dir = dir
cmd.Stderr = os.Stderr
// Add openclaw CLI bin dir to PATH so subprocesses can exec 'openclaw' command
if SimulatorDir != "" {
binDir := filepath.Join(SimulatorDir, "bin")
if info, err := os.Stat(binDir); err == nil && info.IsDir() {
env := os.Environ()
binDirPath := binDir + string(os.PathListSeparator)
for i, e := range env {
if strings.HasPrefix(e, "PATH=") {
env[i] = "PATH=" + binDirPath + e[5:]
break
}
}
cmd.Env = env
}
}
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, fmt.Errorf("stdin pipe: %w", err)