diff --git a/internal/plugins/clawhubadapter/manager/main.js b/internal/plugins/clawhubadapter/manager/main.js index 0efdc0d..e379eb4 100644 --- a/internal/plugins/clawhubadapter/manager/main.js +++ b/internal/plugins/clawhubadapter/manager/main.js @@ -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 '); + 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 '); + 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 '); + // 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 '); + // 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 [args] + +Commands: + plugin:install Install a plugin (npm:xxx, clawhub:xxx, or path) + plugin:uninstall Uninstall a plugin + plugin:list List installed plugins + auth:login [url] Show QR code for login/binding + auth:status Check authentication status + config:get Get config value + config:set 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 \n'); + process.stderr.write('[manager] usage: node main.js [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, diff --git a/internal/plugins/clawhubadapter/manager/openclaw_cli.js b/internal/plugins/clawhubadapter/manager/openclaw_cli.js new file mode 100644 index 0000000..f56f90a --- /dev/null +++ b/internal/plugins/clawhubadapter/manager/openclaw_cli.js @@ -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)); diff --git a/internal/plugins/clawhubadapter/plugin.go b/internal/plugins/clawhubadapter/plugin.go index 418396c..28c8ddf 100644 --- a/internal/plugins/clawhubadapter/plugin.go +++ b/internal/plugins/clawhubadapter/plugin.go @@ -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 } diff --git a/internal/plugins/clawhubadapter/pysimulator/main.py b/internal/plugins/clawhubadapter/pysimulator/main.py index 9c36b91..6a2bbdd 100644 --- a/internal/plugins/clawhubadapter/pysimulator/main.py +++ b/internal/plugins/clawhubadapter/pysimulator/main.py @@ -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(): diff --git a/internal/plugins/clawhubadapter/sidecar.go b/internal/plugins/clawhubadapter/sidecar.go index d54c178..6f694e8 100644 --- a/internal/plugins/clawhubadapter/sidecar.go +++ b/internal/plugins/clawhubadapter/sidecar.go @@ -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)