@sirmrmarty/n8n-nodes-tmux-orchestrator
Version:
n8n nodes for orchestrating Claude AI agents through tmux sessions
161 lines • 8.05 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SecureTmuxWrapper = void 0;
const commandSanitizer_1 = require("./commandSanitizer");
const child_process_1 = require("child_process");
const util_1 = require("util");
const execAsync = (0, util_1.promisify)(child_process_1.exec);
class SecureTmuxWrapper {
static async executeSecurely(command, args, options = {}) {
const startTime = new Date().toISOString();
try {
const sanitized = commandSanitizer_1.CommandSanitizer.prepareTmuxExecution(command, args);
const fullCommand = this.buildCommand(sanitized);
const execOptions = {
timeout: options.timeout || this.DEFAULT_TIMEOUT,
maxBuffer: options.maxBuffer || this.DEFAULT_MAX_BUFFER,
cwd: options.cwd,
env: { ...process.env, ...options.env }
};
const { stdout, stderr } = await execAsync(fullCommand, execOptions);
return {
success: true,
stdout: stdout || '',
stderr: stderr || '',
command: fullCommand,
timestamp: startTime
};
}
catch (error) {
return {
success: false,
stdout: error.stdout || '',
stderr: error.stderr || error.message || 'Unknown error',
command: `${this.TMUX_BINARY} ${command} ${args.join(' ')}`,
timestamp: startTime
};
}
}
static async sendKeysSecurely(sessionName, windowIndex, keys, options = {}) {
const target = commandSanitizer_1.CommandSanitizer.createSafeTmuxTarget(sessionName, windowIndex);
const sanitizedKeys = this.sanitizeKeys(keys);
return this.executeSecurely('send-keys', ['-t', target, sanitizedKeys], options);
}
static async sendCommandSecurely(sessionName, windowIndex, command, options = {}) {
const target = commandSanitizer_1.CommandSanitizer.createSafeTmuxTarget(sessionName, windowIndex);
const sanitizedCommand = this.sanitizeKeys(command);
return this.executeSecurely('send-keys', ['-t', target, sanitizedCommand, 'Enter'], options);
}
static async createSessionSecurely(sessionName, initialDirectory, options = {}) {
const sessionValidation = commandSanitizer_1.CommandSanitizer.validateSessionName(sessionName);
if (!sessionValidation.isValid) {
throw new Error(`Invalid session name: ${sessionValidation.errors.join(', ')}`);
}
const args = ['-d', '-s', sessionName];
if (initialDirectory) {
const pathValidation = commandSanitizer_1.CommandSanitizer.sanitizeFilePath(initialDirectory);
if (!pathValidation.isValid) {
throw new Error(`Invalid directory path: ${pathValidation.errors.join(', ')}`);
}
args.push('-c', pathValidation.path);
}
return this.executeSecurely('new-session', args, options);
}
static async createWindowSecurely(sessionName, windowName, initialDirectory, options = {}) {
const sessionValidation = commandSanitizer_1.CommandSanitizer.validateSessionName(sessionName);
if (!sessionValidation.isValid) {
throw new Error(`Invalid session name: ${sessionValidation.errors.join(', ')}`);
}
const windowValidation = commandSanitizer_1.CommandSanitizer.validateProjectName(windowName);
if (!windowValidation.isValid) {
throw new Error(`Invalid window name: ${windowValidation.errors.join(', ')}`);
}
const args = ['-t', sessionName, '-n', windowName];
if (initialDirectory) {
const pathValidation = commandSanitizer_1.CommandSanitizer.sanitizeFilePath(initialDirectory);
if (!pathValidation.isValid) {
throw new Error(`Invalid directory path: ${pathValidation.errors.join(', ')}`);
}
args.push('-c', pathValidation.path);
}
return this.executeSecurely('new-window', args, options);
}
static async capturePaneSecurely(sessionName, windowIndex, startLine, endLine, options = {}) {
const target = commandSanitizer_1.CommandSanitizer.createSafeTmuxTarget(sessionName, windowIndex);
const args = ['-t', target, '-p'];
if (startLine !== undefined && endLine !== undefined) {
if (!Number.isInteger(startLine) || !Number.isInteger(endLine) ||
startLine < 0 || endLine < 0 || startLine > endLine) {
throw new Error('Invalid line range: start and end must be non-negative integers with start <= end');
}
args.push('-S', startLine.toString(), '-E', endLine.toString());
}
return this.executeSecurely('capture-pane', args, options);
}
static async listSessionsSecurely(options = {}) {
return this.executeSecurely('list-sessions', ['-F', '#{session_name}:#{session_windows}:#{session_created}'], options);
}
static async listWindowsSecurely(sessionName, options = {}) {
const sessionValidation = commandSanitizer_1.CommandSanitizer.validateSessionName(sessionName);
if (!sessionValidation.isValid) {
throw new Error(`Invalid session name: ${sessionValidation.errors.join(', ')}`);
}
return this.executeSecurely('list-windows', ['-t', sessionName, '-F', '#{window_index}:#{window_name}:#{window_active}'], options);
}
static async killSessionSecurely(sessionName, options = {}) {
const sessionValidation = commandSanitizer_1.CommandSanitizer.validateSessionName(sessionName);
if (!sessionValidation.isValid) {
throw new Error(`Invalid session name: ${sessionValidation.errors.join(', ')}`);
}
return this.executeSecurely('kill-session', ['-t', sessionName], options);
}
static async hasSessionSecurely(sessionName, options = {}) {
const sessionValidation = commandSanitizer_1.CommandSanitizer.validateSessionName(sessionName);
if (!sessionValidation.isValid) {
throw new Error(`Invalid session name: ${sessionValidation.errors.join(', ')}`);
}
const result = await this.executeSecurely('has-session', ['-t', sessionName], options);
return result.success;
}
static async killWindowSecurely(sessionName, windowIndex, options = {}) {
const target = commandSanitizer_1.CommandSanitizer.createSafeTmuxTarget(sessionName, windowIndex);
return this.executeSecurely('kill-window', ['-t', target], options);
}
static buildCommand(sanitized) {
const escapedArgs = sanitized.args.map(arg => this.escapeShellArgument(arg));
return `${this.TMUX_BINARY} ${sanitized.command} ${escapedArgs.join(' ')}`;
}
static sanitizeKeys(keys) {
if (!keys || typeof keys !== 'string') {
throw new Error('Keys must be a non-empty string');
}
let sanitized = keys
.replace(/[;&|`$(){}]/g, '')
.replace(/\\/g, '\\\\')
.trim();
if (sanitized.length === 0) {
throw new Error('Keys cannot be empty after sanitization');
}
if (sanitized.length > 10000) {
throw new Error('Keys exceed maximum length of 10000 characters');
}
return sanitized;
}
static escapeShellArgument(arg) {
return `'${arg.replace(/'/g, "'\"'\"'")}'`;
}
static async validateTmuxAvailable() {
try {
const result = await execAsync(`${this.TMUX_BINARY} -V`, { timeout: 5000 });
return result.stdout.includes('tmux');
}
catch (error) {
return false;
}
}
}
exports.SecureTmuxWrapper = SecureTmuxWrapper;
SecureTmuxWrapper.DEFAULT_TIMEOUT = 30000;
SecureTmuxWrapper.DEFAULT_MAX_BUFFER = 1024 * 1024;
SecureTmuxWrapper.TMUX_BINARY = 'tmux';
//# sourceMappingURL=secureTmuxWrapper.js.map