#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); function parseArgs(argv) { const args = { toolArgs: [] }; for (let i = 0; i < argv.length; i++) { const token = argv[i]; if (token === '--') { args.toolArgs = argv.slice(i + 1); break; } if (token === '--tool') { args.tool = argv[++i]; continue; } if (token === '--result-prefix') { args.resultPrefix = argv[++i]; continue; } } return args; } function usage() { return [ 'Usage:', ' node openclaw-tool-runner.js --tool --result-prefix -- [tool args]', '', 'The runner resolves tools from ~/.openclaw/tools first and emits PREFIX= after the child tool output.' ].join('\n'); } function unique(values) { return Array.from(new Set(values.filter(Boolean))); } function resolveTool(tool) { if (!tool) return undefined; if (path.isAbsolute(tool) && fs.existsSync(tool)) return tool; const cwd = process.cwd(); const toolRoot = __dirname; const projectRoot = path.resolve(__dirname, '..', '..'); const candidates = unique([ path.join(toolRoot, tool), path.join(cwd, tool), path.join(cwd, 'scripts', 'tools', tool), path.join(projectRoot, 'scripts', 'tools', tool), path.join(projectRoot, tool) ]); return candidates.find(candidate => fs.existsSync(candidate)); } function extractLastJson(stdout) { const text = String(stdout || '').trim(); if (!text) return undefined; for (let i = text.lastIndexOf('{'); i >= 0; i = text.lastIndexOf('{', i - 1)) { const candidate = text.slice(i).trim(); try { return JSON.parse(candidate); } catch { continue; } } return undefined; } function isUnresolvedValue(value) { if (value === undefined || value === null) return true; const text = String(value).trim(); return text === '' || /^\{\{[^}]+\}\}$/.test(text); } function sanitizeToolArgs(toolArgs) { const sanitized = []; for (let i = 0; i < toolArgs.length; i++) { const token = toolArgs[i]; const next = toolArgs[i + 1]; if (token.startsWith('--') && next !== undefined && !next.startsWith('--')) { if (!isUnresolvedValue(next)) { sanitized.push(token, next); } i++; continue; } if (!isUnresolvedValue(token)) { sanitized.push(token); } } return sanitized; } function main() { const args = parseArgs(process.argv.slice(2)); if (!args.tool || !args.resultPrefix || args.help) { console.log(usage()); process.exit(args.help ? 0 : 1); } const toolPath = resolveTool(args.tool); if (!toolPath) { const result = { status: 'error', message: `Tool not found: ${args.tool}`, searchedFrom: { cwd: process.cwd(), runnerDir: __dirname } }; console.log(`${args.resultPrefix}=${JSON.stringify(result)}`); process.exit(1); } const child = spawnSync(process.execPath, [toolPath, ...sanitizeToolArgs(args.toolArgs)], { cwd: process.cwd(), encoding: 'utf8', maxBuffer: 1024 * 1024 * 100 }); if (child.stdout) process.stdout.write(child.stdout); if (child.stderr) process.stderr.write(child.stderr); const parsed = extractLastJson(child.stdout); const result = parsed || { status: child.status === 0 ? 'ok' : 'error', tool: args.tool, toolPath, message: parsed ? undefined : 'Tool did not emit parseable JSON on stdout' }; console.log(`${args.resultPrefix}=${JSON.stringify(result)}`); if (child.error) { console.error(child.error.message); process.exit(1); } process.exit(child.status === null || child.status === undefined ? 1 : child.status); } main();