Files
HomeAgent/internal/plugins/clawhubadapter/manager/main.js
root 4b6caae605 feat(clawhubadapter): bridge OC channels to HomeAgent output/input channels
- registry.go: replace channelDevice+RegisterChannel with RegisterOutputChannel
  handler routes output via sp.CallTool to JS manager outbound handlers
  register {plugin}_read_{ch}_input tool for agent to poll buffered input
- plugin.go: handle channel_input notifications in translateAndRegister
  buffer payload in channelInputBuf + InjectInterruptText to notify agent
- manager/main.js: registerChannel supports {plugin: ChannelPlugin} format
  tools/call routes channel names to outbound.sendText/sendMedia
  submitInput sends channel_input notification to Go side
2026-07-23 14:45:10 +08:00

734 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// ---- Utility ----
function writeJSON(obj) {
process.stdout.write(JSON.stringify(obj) + '\n');
}
function sendError(id, code, message) {
writeJSON({ jsonrpc: '2.0', id, error: { code, message } });
}
function notify(method, params) {
writeJSON({ jsonrpc: '2.0', method, params });
}
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
const allProviders = {}; // type -> { name, instance } across all plugins
function registerPluginTools(name, tools, api) {
for (const t of tools) {
if (t && t.name) {
t._plugin = name;
allTools.push(t);
notify('register', { type: 'tool', data: { name: t.name, description: t.description, parameters: t.parameters } });
}
}
loadedPlugins[name] = { tools, api };
}
function loadPlugin(pluginDir, name) {
// Resolve entry
const pkg = readJSON(path.join(pluginDir, 'package.json'));
let entryPath = null;
if (pkg && pkg.openclaw) {
let raw = pkg.openclaw.runtimeExtensions || pkg.openclaw.extensions;
if (typeof raw === 'string') raw = [raw];
if (Array.isArray(raw)) {
for (const ext of raw) {
let ep = path.resolve(pluginDir, ext);
if (ep.endsWith('.ts')) { const js = ep.replace(/\.ts$/, '.js'); if (fs.existsSync(js)) { entryPath = js; break; } }
if (fs.existsSync(ep)) { entryPath = ep; break; }
}
}
}
if (!entryPath) {
const manifest = readJSON(path.join(pluginDir, 'openclaw.plugin.json'));
if (manifest) {
const ep = manifest.entry || manifest.main || 'index.js';
entryPath = path.join(pluginDir, ep);
}
}
if (!entryPath) entryPath = path.join(pluginDir, 'index.js');
if (!fs.existsSync(entryPath)) {
process.stderr.write(`[manager] entry not found for ${name}: ${entryPath}\n`);
return false;
}
let pluginEntry;
try { pluginEntry = require(entryPath); } catch (e) {
process.stderr.write(`[manager] load ${name}: ${e.message}\n`);
return false;
}
const entry = pluginEntry.default || pluginEntry;
if (typeof entry !== 'object' || typeof entry.register !== 'function') {
process.stderr.write(`[manager] ${name}: entry must export {register(api)}\n`);
return false;
}
const registeredTools = [];
const api = {
id: name,
name,
version: (pkg && pkg.version) || '1.0.0',
description: (pkg && pkg.description) || '',
source: pluginDir,
rootDir: pluginDir,
config: {},
pluginConfig: {},
registrationMode: 'full',
logger: { debug: () => {}, info: () => {}, warn: () => {}, error: (...args) => process.stderr.write(`[${name}] ${args.join(' ')}\n`) },
resolvePath: (p) => path.resolve(pluginDir, p),
registerTool: (def, opts) => {
if (typeof def === 'function') {
const toolCtx = { id: name, cwd: pluginDir, env: process.env, allow: ['*'] };
const result = def(toolCtx);
const tools = Array.isArray(result) ? result : [result];
for (const t of tools) { if (t && typeof t.execute === 'function') registeredTools.push(t); }
return;
}
if (!def || !def.name) return;
registeredTools.push({ name: def.name, label: def.label || def.name, description: def.description || '', parameters: def.parameters || { type: 'object', properties: {} }, execute: typeof def.execute === 'function' ? def.execute : undefined });
},
registerProvider: (p) => {
if (p && p.id) allProviders['llm'] = { name: p.id, instance: p };
notify('register', { type: 'provider', data: { name: p?.id || p?.name } });
},
registerChannel: (ch) => {
let chName = ch.name;
let chType = ch.type || 'text';
const chPlugin = ch.plugin;
// OpenClaw ChannelPlugin 格式: { plugin: { id, outbound: { sendText, sendMedia }, ... } }
if (chPlugin && typeof chPlugin === 'object') {
chName = chName || chPlugin.id || chPlugin.meta?.id || name + '-channel';
chType = chType || (chPlugin.capabilities?.media ? 'io' : 'text');
registeredChannels[chName] = { pluginName: name, channelPlugin: chPlugin, type: chType };
} else {
// 简单格式: { name, type, output }
registeredChannels[chName] = { pluginName: name, output: ch.output || ch.send, type: chType };
}
notify('register', { type: 'channel', data: { name: chName, type: chType, id: chPlugin?.id } });
},
submitInput: (msg) => {
notify('channel_input', { channel: name, payload: msg });
},
registerHook: (hook) => notify('register', { type: 'hook', data: { name: hook.name, event: hook.event } }),
registerHttpRoute: (route) => notify('register', { type: 'http_route', data: { path: route.path, method: route.method } }),
registerCommand: (cmd) => notify('register', { type: 'command', data: { name: cmd.name, description: cmd.description } }),
registerService: (svc) => notify('register', { type: 'service', data: { name: svc.name } }),
registerImageGenerationProvider: (p) => {
if (p) allProviders['image_generation'] = { name: p.name, instance: p };
notify('register', { type: 'image_generation_provider', data: { name: p?.name } });
},
registerWebFetchProvider: (p) => {
if (p) allProviders['web_fetch'] = { name: p.name, instance: p };
notify('register', { type: 'web_fetch_provider', data: { name: p?.name } });
},
registerWebSearchProvider: (p) => {
if (p) allProviders['web_search'] = { name: p.name, instance: p };
notify('register', { type: 'web_search_provider', data: { name: p?.name } });
},
start: (cb) => {},
stop: (cb) => {},
};
entry.register(api);
registerPluginTools(name, registeredTools, api);
process.stderr.write(`[manager] loaded plugin: ${name} (${registeredTools.length} tools)\n`);
return true;
}
// ---- Install npm package ----
function installNPMPackage(spec, skillsDir) {
process.stderr.write(`[manager] installing: ${spec}\n`);
const installDir = path.join(skillsDir, '.npm_install_' + Date.now());
fs.mkdirSync(installDir, { recursive: true });
try {
execSync(`npm install ${spec} --no-save --prefix "${installDir}"`, {
cwd: installDir, stdio: ['pipe', 'pipe', 'pipe'],
timeout: 120000, env: { ...process.env, NODE_PATH: path.join(installDir, 'node_modules') }
});
} catch (e) {
fs.rmSync(installDir, { recursive: true, force: true });
return { error: e.stderr ? e.stderr.toString() : e.message };
}
const nm = path.join(installDir, 'node_modules');
if (!fs.existsSync(nm)) {
fs.rmSync(installDir, { recursive: true, force: true });
return { error: 'node_modules not created' };
}
let foundPluginDir = null;
let foundName = null;
const entries = fs.readdirSync(nm);
for (const entry of entries) {
const dir = path.join(nm, entry);
if (!fs.statSync(dir).isDirectory()) continue;
if (entry.startsWith('@')) {
const subs = fs.readdirSync(dir);
for (const sub of subs) {
const subDir = path.join(dir, sub);
if (fs.existsSync(path.join(subDir, 'openclaw.plugin.json')) ||
(fs.existsSync(path.join(subDir, 'package.json')) && readJSON(path.join(subDir, 'package.json'))?.openclaw)) {
foundPluginDir = subDir;
foundName = entry + '/' + sub;
}
}
continue;
}
if (fs.existsSync(path.join(dir, 'openclaw.plugin.json')) ||
(fs.existsSync(path.join(dir, 'package.json')) && readJSON(path.join(dir, 'package.json'))?.openclaw)) {
foundPluginDir = dir;
foundName = entry;
}
}
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}"` };
}
// Copy to skills dir
const targetDir = path.join(skillsDir, foundName);
if (fs.existsSync(targetDir)) fs.rmSync(targetDir, { recursive: true, force: true });
cpSync(foundPluginDir, targetDir);
fs.rmSync(installDir, { recursive: true, force: true });
return { name: foundName, dir: targetDir };
}
function cpSync(src, dst) {
fs.mkdirSync(dst, { recursive: true });
for (const entry of fs.readdirSync(src)) {
const s = path.join(src, entry);
const d = path.join(dst, entry);
if (fs.statSync(s).isDirectory()) {
cpSync(s, d);
} else {
fs.copyFileSync(s, d);
}
}
}
// ---- 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> [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;
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'))) {
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);
}
}
}
}
// ---- JSON-RPC ----
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
rl.on('line', async (line) => {
let req;
try { req = JSON.parse(line); } catch { sendError(null, -32700, 'Parse error'); return; }
const id = req.id;
const method = req.method;
if (method === 'ping') {
writeJSON({ jsonrpc: '2.0', id, result: { status: 'ok', plugins: Object.keys(loadedPlugins).length } });
return;
}
if (method === 'plugins/list') {
const list = Object.entries(loadedPlugins).map(([name, p]) => ({
name,
tools: p.tools.map(t => ({ name: t.name, description: t.description })),
}));
writeJSON({ jsonrpc: '2.0', id, result: { plugins: list } });
return;
}
if (method === 'plugins/install') {
const pkg = req.params?.package;
if (!pkg) { sendError(id, -32602, 'package required'); return; }
const result = installNPMPackage(pkg, skillsDir);
if (result.error) {
sendError(id, -32603, result.error);
return;
}
// Load the newly installed plugin
const ok = loadPlugin(result.dir, result.name);
if (!ok) {
sendError(id, -32603, `failed to load installed plugin: ${result.name}`);
return;
}
writeJSON({ jsonrpc: '2.0', id, result: { name: result.name, tools: loadedPlugins[result.name].tools.map(t => t.name) } });
return;
}
if (method === 'plugins/uninstall') {
const name = req.params?.name;
if (!name) { sendError(id, -32602, 'name required'); return; }
if (!loadedPlugins[name]) { sendError(id, -32601, `plugin not found: ${name}`); return; }
// Remove tools
const idxs = [];
for (let i = allTools.length - 1; i >= 0; i--) {
if (allTools[i]._plugin === name) allTools.splice(i, 1);
}
delete loadedPlugins[name];
// Remove directory
const pluginDir = path.join(skillsDir, name);
if (fs.existsSync(pluginDir)) fs.rmSync(pluginDir, { recursive: true, force: true });
writeJSON({ jsonrpc: '2.0', id, result: { status: 'uninstalled', name } });
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,
description: t.description || '',
inputSchema: t.parameters || { type: 'object', properties: {} },
}));
writeJSON({ jsonrpc: '2.0', id, result: { tools } });
return;
}
if (method === 'tools/call') {
const params = req.params || {};
const toolName = params.name;
const args = params.arguments || {};
// 通道输出路由toolName 匹配已注册通道名时,调通道的输出 handler
const ch = registeredChannels[toolName];
if (ch) {
try {
const channelPlugin = ch.channelPlugin;
if (channelPlugin && channelPlugin.outbound) {
const meta = args.meta || '';
let metaObj = {};
try { metaObj = typeof meta === 'string' ? JSON.parse(meta) : meta; } catch {}
const to = metaObj.user_id || metaObj.to || metaObj.group_id || '';
const ctx = { to, text: args.payload || '', mediaUrl: metaObj.mediaUrl || '', cfg: {}, accountId: metaObj.accountId || null };
let result;
if (ctx.mediaUrl && channelPlugin.outbound.sendMedia) {
result = await channelPlugin.outbound.sendMedia(ctx);
} else if (channelPlugin.outbound.sendText) {
result = await channelPlugin.outbound.sendText(ctx);
} else {
throw new Error(`channel ${toolName} has no sendText/sendMedia handler`);
}
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', result } });
} else if (typeof ch.output === 'function') {
const result = await ch.output(args.payload, args.type, args.meta);
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', result } });
} else if (typeof ch.send === 'function') {
const result = await ch.send(args.payload, args.meta);
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', result } });
} else {
sendError(id, -32601, `channel ${toolName} has no output handler`);
}
} catch (e) { sendError(id, -32603, e.message); }
return;
}
const tool = allTools.find(t => t.name === toolName);
if (!tool) { sendError(id, -32601, `Tool not found: ${toolName}`); return; }
if (typeof tool.execute !== 'function') { sendError(id, -32603, `Tool ${toolName} has no execute`); return; }
try {
const result = await tool.execute('mgr-call-1', args, undefined, undefined);
if (result && typeof result === 'object' && Array.isArray(result.content)) {
writeJSON({ jsonrpc: '2.0', id, result });
} else {
const text = typeof result === 'string' ? result : JSON.stringify(result);
writeJSON({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } });
}
} catch (e) { sendError(id, -32603, e.message); }
return;
}
if (method === 'provider/call') {
const { type, action, args } = req.params || {};
if (!type) { sendError(id, -32602, 'type required'); return; }
const provider = allProviders[type];
if (!provider) { sendError(id, -32601, `Provider not found: ${type}`); return; }
const methodName = action || 'execute';
if (typeof provider.instance[methodName] !== 'function') {
sendError(id, -32603, `Provider ${type} has no method ${methodName}`);
return;
}
try {
const result = await provider.instance[methodName](args);
if (result && typeof result === 'object' && Array.isArray(result.content)) {
writeJSON({ jsonrpc: '2.0', id, result });
} else {
const text = typeof result === 'string' ? result : JSON.stringify(result);
writeJSON({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } });
}
} catch (e) { sendError(id, -32603, e.message); }
return;
}
sendError(id, -32601, `Method not found: ${method}`);
});