@monitoro/herd
Version:
Automate your browser, build AI web tools and MCP servers with Monitoro Herd
419 lines (352 loc) โข 16.6 kB
text/typescript
import * as process from 'process';
// Suppress all warnings in Node.js
process.env.NODE_NO_WARNINGS = '1';
// This is a valid way to suppress deprecation warnings
process.env.NODE_OPTIONS = '--no-deprecation --no-warnings';
import { initConfig, getHerdClient } from './config';
initConfig();
import { Command } from 'commander';
import { prompt } from 'promptly';
import { spawn, ChildProcess } from 'child_process';
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import * as chokidar from 'chokidar';
import { execute as executeRun } from '../src/lib/trails/run';
import { execute as executeTest } from '../src/lib/trails/test';
import { execute as executeInit } from '../src/lib/trails/init';
import { HerdClient } from '../src/lib/HerdClient';
import { version } from '../package.json';
const program = new Command();
program
.name('herd')
.description('๐ Herd.garden - A modern web automation platform for developers')
.version(
version
);
// login (and save token to .herdrc)
program
.command('login')
.description('๐ Login to Herd.garden and save your credentials')
.action(async () => {
if (process.env.HERD_API_KEY) {
console.log('โ
You are already logged in!');
process.exit(0);
}
// Ask user to enter their token
const token = await prompt('๐ Enter your Herd.garden token: ');
if (!token) {
console.error('โ Error: No token provided');
process.exit(1);
}
// Save token to .herdrc with proper permissions (so that we can read it later)
try {
// First we try to login with the token
const herdClient = new HerdClient({ token });
await herdClient.initialize();
fs.writeFileSync(path.join(os.homedir(), '.herdrc'), token, { mode: 0o600 });
console.log('๐ Token saved successfully to .herdrc!');
} catch (error) {
console.error('โ Error: Failed to save token to .herdrc', error);
} finally {
process.exit(1);
}
});
program
.command('devices')
.description('๐ฅ๏ธ List all registered devices in your herd')
.action(async () => {
const herdClient = getHerdClient();
console.log('๐ Fetching your devices...');
const devices = await herdClient.listDevices();
// display device[].info as a nice table
console.table(devices.map(device => ({
deviceId: device.deviceId,
name: device.name,
type: device.type,
status: device.status,
lastActive: device.lastActive ? new Date(device.lastActive).toLocaleString() : 'never',
})));
console.log(`\nโจ Found ${devices.length} device${devices.length === 1 ? '' : 's'} in your herd.\n`);
});
// MCP command group
const mcpCommand = program
.command('mcp')
.description('๐ MCP server management and development tools');
mcpCommand
.command('dev')
.description('๐ ๏ธ Start MCP server in development mode with hot reloading')
.action(async () => {
try {
// Get the current working directory
const cwd = process.cwd();
// Read package.json to find the main file
const packageJsonPath = path.join(cwd, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
console.error('โ Error: package.json not found in the current directory');
process.exit(1);
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const mainFile = packageJson.main;
if (!mainFile) {
console.error('โ Error: No "main" field found in package.json');
process.exit(1);
}
// Default ports
const MCP_PORT = process.env.MCP_PORT || 4999;
const INSPECTOR_PORT = process.env.INSPECTOR_PORT || 5999;
let mcpProcess: ChildProcess | null = null;
let inspectorProcess: ChildProcess | null = null;
// Function to start the MCP server
const startMcpServer = () => {
if (mcpProcess) {
mcpProcess.kill();
}
console.log('๐ Starting MCP server...');
mcpProcess = spawn('npx', ['tsx', mainFile], {
stdio: 'inherit',
env: {
...process.env,
NODE_NO_WARNINGS: '1',
NODE_OPTIONS: '--no-deprecation --no-warnings'
}
});
mcpProcess.on('error', (err) => {
console.error('โ Failed to start MCP server:', err);
});
};
// Function to start the inspector
const startInspector = () => {
if (inspectorProcess) {
inspectorProcess.kill();
}
console.log('๐ Starting MCP inspector...');
inspectorProcess = spawn('npx', ['@modelcontextprotocol/inspector'], {
stdio: 'inherit',
env: {
...process.env,
PORT: INSPECTOR_PORT.toString(),
NODE_NO_WARNINGS: '1',
NODE_OPTIONS: '--no-deprecation --no-warnings'
}
});
inspectorProcess.on('error', (err) => {
console.error('โ Failed to start MCP inspector:', err);
});
// Construct the inspector URL
const serverUrl = encodeURIComponent(`http://localhost:${MCP_PORT}/sse`);
console.log(`\nโจ MCP Inspector URL: http://localhost:${INSPECTOR_PORT}/?serverUrl=${serverUrl}\n`);
};
// Start both processes
startMcpServer();
startInspector();
// Watch for file changes
const watcher = chokidar.watch(['./**/*.ts', './**/*.js'], {
ignored: ['**/node_modules/**', '**/dist/**'],
persistent: true
});
console.log('๐ Watching for file changes...');
watcher.on('change', (path) => {
console.log(`\n๐ File ${path} has been changed. Restarting...`);
startMcpServer();
startInspector();
});
// Handle process termination
const cleanup = () => {
if (mcpProcess) mcpProcess.kill();
if (inspectorProcess) inspectorProcess.kill();
console.log('\n๐ Shutting down MCP development server...');
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
console.log('\n๐ MCP development environment is ready!');
} catch (error) {
console.error('โ Error:', error);
process.exit(1);
}
});
// Trail command group
const trailCommand = program
.command('trail')
.description('๐ Herd.garden trail management - create, run, and test your trails');
trailCommand
.command('init')
.description('๐๏ธ Create a new trail with everything you need to get started')
.action(async () => {
await executeInit();
});
trailCommand
.command('run')
.description('๐ Run a trail to perform automated actions on the web')
.option('-t, --trail <trailPath>', 'The trail to run (defaults to current directory)')
.option('-a, --action <actionName>', 'The action to run')
.option('-p, --params <params>', 'The params to pass to the action')
// .option('-d, --device <deviceId>', 'The device to run the trail on')
.option('-o, --output-only', 'Output only the result, no colors or formatting')
.action(async (options) => {
const herdClient = getHerdClient();
try {
const trailPath = getTrailPath(options);
console.log(`๐ Running trail action ${options.action || 'default'} from ${path.basename(trailPath)}...`);
// load the trail
const result = await executeRun(herdClient, trailPath, options.action, options.params ? JSON.parse(options.params) : {});
if (options.outputOnly) {
console.log(JSON.stringify(result, null, 2));
} else {
// nice colorful output of result
console.log("\nโจ Result:");
console.log("\x1b[33m%s\x1b[0m", JSON.stringify(result, null, 2));
console.log("\n๐ Trail run completed successfully!");
}
} catch (error) {
console.error('\nโ Error running trail:', error);
process.exit(1);
}
});
trailCommand
.command('test')
.description('๐งช Test a trail to ensure it works correctly')
.option('-t, --trail <trailPath>', 'The trail to test (defaults to current directory)')
.option('-s, --selector <selectorId>', 'The selector to test (either this or action must be provided)')
.option('-a, --action <actionName>', 'The action to test (either this or selector must be provided)')
.option('-p, --params <params>', 'The params to pass to the action (defaults to manifest params)')
// .option('-d, --device <deviceId>', 'The device to test the trail on')
.option('-w, --watch', 'Watch for changes in the trail folder and re-run tests')
.option('-k, --keep-alive', 'Do not exit on error (defaults to true in watch mode)')
.action(async (options) => {
const herdClient = getHerdClient();
// Always try to register the TypeScript loader since tsx is now a direct dependency
try {
// Dynamically import the tsHook module
const { registerTypeScriptLoader } = await import('../src/lib/trails/tsHook');
const registered = registerTypeScriptLoader();
if (registered) {
console.log('โ
TypeScript loader registered successfully');
} else {
console.warn('โ ๏ธ Failed to register TypeScript loader - falling back to JavaScript files if available');
}
} catch (e) {
console.error('โ Error loading TypeScript hook module', e);
}
try {
const trailPath = getTrailPath(options);
if (options.watch) {
console.log(`๐ Watching trail for changes in ${trailPath} ...`);
// Watch the entire trail directory
const watcher = chokidar.watch(path.resolve(trailPath), {
ignored: ['**/node_modules/**', '**/dist/**'],
persistent: true,
ignoreInitial: true
});
// Run the initial test
runChildProcess();
watcher.on('change', (changedPath) => {
console.log(`\n๐ Changes detected in ${changedPath}. Running tests...`);
console.clear();
runChildProcess();
});
function runChildProcess() {
// Build the command arguments without the watch flag but with keep-alive
const args = ['trail', 'test'];
if (options.trail) args.push('-t', options.trail);
if (options.selector) args.push('-s', options.selector);
if (options.action) args.push('-a', options.action);
if (options.params) args.push('-p', options.params);
args.push('-k'); // Add keep-alive flag
// Use the same command that was used to run this script
// If we're using tsx, we need to use it for the child process too
const executable = process.argv[0]; // node or other interpreter
const scriptPath = process.argv[1]; // path to cli.ts
// Check if we're using tsx
const usingTsx = process.execArgv.some(arg => arg.includes('tsx'));
let command;
let commandArgs;
if (usingTsx) {
// If using tsx, we need to run with tsx
command = executable;
commandArgs = ['--import', 'tsx', scriptPath, ...args];
} else {
command = executable;
commandArgs = [scriptPath, ...args];
}
console.log(`๐งช Running: ${command} ${commandArgs.join(' ')}`);
const child = spawn(command, commandArgs, {
stdio: 'inherit'
});
child.on('error', (error) => {
console.error(`โ Error running test: ${error.message}`);
});
}
} else {
if (options.selector) {
console.log(`๐งช Testing selector '${options.selector}' from ${path.basename(trailPath)}...`);
} else if (options.action) {
console.log(`๐งช Testing action '${options.action}' from ${path.basename(trailPath)}...`);
}
let result;
try {
if (options.action) {
result = await executeTest(herdClient, trailPath, { actionName: options.action });
} else if (options.selector) {
result = await executeTest(herdClient, trailPath, { selectorId: options.selector });
} else {
console.error('โ Error: No action or selector provided');
process.exit(1);
}
if (result && result.status === "success") {
console.log("\nโ
Test completed successfully!");
} else if (result && result.status === "error" && !options.keepAlive) {
console.error("\nโ Test failed!");
process.exit(1);
}
} catch (err: any) {
// Enhanced error handling
if (err.message && err.message.includes('Unexpected token')) {
console.error(`
โ TypeScript loading error: Unable to parse TypeScript files directly.
Please either:
1. Install tsx globally: npm install -g tsx
2. Run with the --use-tsx flag: herd trail test --use-tsx -a ${options.action || options.selector}
3. Compile your TypeScript files to JavaScript first: tsc
Original error: ${err.message}
`);
} else {
console.error('\nโ Error testing trail:', err);
}
if (!options.keepAlive) {
process.exit(1);
}
}
}
} catch (error) {
console.error('โ Error testing trail:', error);
if (!options.keepAlive) {
process.exit(1);
}
}
});
// Let's also update the getTrailPath function to have nicer error messages
function getTrailPath(options: any) {
let trailPath = process.cwd();
// Did we get a trail name? pick this one otherwise try current directory
if (options.trail) {
trailPath = path.join(process.cwd(), options.trail);
}
// Check for both TS and JS files
const hasTypeScriptFiles = fs.existsSync(path.join(trailPath, "actions.ts")) &&
fs.existsSync(path.join(trailPath, "urls.ts")) &&
fs.existsSync(path.join(trailPath, "selectors.ts"));
const hasJavaScriptFiles = fs.existsSync(path.join(trailPath, "actions.js")) &&
fs.existsSync(path.join(trailPath, "urls.js")) &&
fs.existsSync(path.join(trailPath, "selectors.js"));
// are we inside a trail directory?
if (!hasTypeScriptFiles && !hasJavaScriptFiles) {
console.error('โ No valid trail found! Missing required files (actions, urls, selectors)');
console.log('๐ก Tip: Run "herd trail init" to create a new trail, or cd into an existing trail directory');
process.exit(1);
}
return trailPath;
}
program.parse(process.argv);