@sirmrmarty/n8n-nodes-tmux-orchestrator
Version:
n8n nodes for orchestrating Claude AI agents through tmux sessions
735 lines • 33.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TmuxAgentMonitor = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const tmuxBridge_1 = require("../../utils/tmuxBridge");
const fs_1 = require("fs");
class TmuxAgentMonitor {
constructor() {
this.description = {
displayName: 'Tmux Agent Monitor',
name: 'tmuxAgentMonitor',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"]}}',
description: 'Monitor and analyze tmux agent activity and health',
defaults: {
name: 'Tmux Agent Monitor',
},
inputs: ["main"],
outputs: ["main"],
credentials: [
{
name: 'tmuxOrchestratorApi',
required: false,
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'List All Sessions',
value: 'listAllSessions',
description: 'Get all active tmux sessions with details',
},
{
name: 'Health Check',
value: 'healthCheck',
description: 'Check agent responsiveness and health',
},
{
name: 'Collect Logs',
value: 'collectLogs',
description: 'Aggregate agent conversation logs',
},
{
name: 'Detect Blockers',
value: 'detectBlockers',
description: 'Identify stuck or blocked agents',
},
{
name: 'Monitor Snapshot',
value: 'monitorSnapshot',
description: 'Create comprehensive monitoring snapshot',
},
{
name: 'Find Windows',
value: 'findWindows',
description: 'Find windows by name pattern',
},
{
name: 'Activity Report',
value: 'activityReport',
description: 'Generate activity report for sessions',
},
{
name: 'Check Subagent Opportunities',
value: 'checkSubagentOpportunities',
description: 'Identify agents that could benefit from subagent help',
},
],
default: 'listAllSessions',
},
{
displayName: 'Target Sessions',
name: 'targetSessions',
type: 'string',
default: '',
displayOptions: {
show: {
operation: ['healthCheck', 'collectLogs', 'detectBlockers', 'activityReport'],
},
},
description: 'Comma-separated list of sessions to check (leave empty for all)',
},
{
displayName: 'Response Timeout',
name: 'responseTimeout',
type: 'number',
default: 10,
displayOptions: {
show: {
operation: ['healthCheck'],
},
},
description: 'Seconds to wait for agent response',
},
{
displayName: 'Lines Per Window',
name: 'linesPerWindow',
type: 'number',
default: 100,
displayOptions: {
show: {
operation: ['collectLogs'],
},
},
description: 'Number of lines to collect from each window',
},
{
displayName: 'Save to File',
name: 'saveToFile',
type: 'boolean',
default: false,
displayOptions: {
show: {
operation: ['collectLogs'],
},
},
description: 'Save logs to files',
},
{
displayName: 'Output Directory',
name: 'outputDirectory',
type: 'string',
default: '/tmp/tmux-logs',
displayOptions: {
show: {
operation: ['collectLogs'],
saveToFile: [true],
},
},
description: 'Directory to save log files',
},
{
displayName: 'Inactivity Threshold',
name: 'inactivityThreshold',
type: 'number',
default: 300,
displayOptions: {
show: {
operation: ['detectBlockers'],
},
},
description: 'Seconds of inactivity to consider agent blocked',
},
{
displayName: 'Error Keywords',
name: 'errorKeywords',
type: 'string',
default: 'error,failed,exception,blocked,stuck,waiting',
displayOptions: {
show: {
operation: ['detectBlockers'],
},
},
description: 'Comma-separated keywords to detect issues',
},
{
displayName: 'Window Pattern',
name: 'windowPattern',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: ['findWindows'],
},
},
description: 'Pattern to search for in window names',
},
{
displayName: 'Report Period',
name: 'reportPeriod',
type: 'options',
options: [
{
name: 'Last Hour',
value: 'hour',
},
{
name: 'Last 24 Hours',
value: 'day',
},
{
name: 'Last Week',
value: 'week',
},
{
name: 'All Time',
value: 'all',
},
],
default: 'hour',
displayOptions: {
show: {
operation: ['activityReport'],
},
},
description: 'Time period for activity report',
},
],
};
}
async execute() {
const items = this.getInputData();
const returnData = [];
const operation = this.getNodeParameter('operation', 0);
let bridgeConfig = {};
try {
const credentials = await this.getCredentials('tmuxOrchestratorApi');
if (credentials?.useExternalScripts && credentials?.scriptsDirectory) {
bridgeConfig.externalScriptsDir = credentials.scriptsDirectory;
}
if (credentials?.projectBasePath) {
bridgeConfig.projectBasePath = credentials.projectBasePath;
}
}
catch {
}
const bridge = new tmuxBridge_1.TmuxBridge(bridgeConfig);
for (let i = 0; i < items.length; i++) {
try {
let result = {};
switch (operation) {
case 'listAllSessions':
result = await TmuxAgentMonitor.prototype.listAllSessions(this, bridge);
break;
case 'healthCheck':
result = await TmuxAgentMonitor.prototype.healthCheck(this, i, bridge);
break;
case 'collectLogs':
result = await TmuxAgentMonitor.prototype.collectLogs(this, i, bridge);
break;
case 'detectBlockers':
result = await TmuxAgentMonitor.prototype.detectBlockers(this, i, bridge);
break;
case 'monitorSnapshot':
result = await TmuxAgentMonitor.prototype.monitorSnapshot(this, bridge);
break;
case 'findWindows':
result = await TmuxAgentMonitor.prototype.findWindows(this, i, bridge);
break;
case 'activityReport':
result = await TmuxAgentMonitor.prototype.activityReport(this, i, bridge);
break;
case 'checkSubagentOpportunities':
result = await TmuxAgentMonitor.prototype.checkSubagentOpportunities(this, i, bridge);
break;
}
returnData.push({
json: result,
pairedItem: i,
});
}
catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: { error: error.message },
pairedItem: i,
});
}
else {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, {
itemIndex: i,
});
}
}
}
return [returnData];
}
async listAllSessions(context, bridge) {
try {
const sessions = await bridge.getTmuxSessions();
const sessionDetails = sessions.map(session => ({
name: session.name,
attached: session.attached,
windowCount: session.windows.length,
windows: session.windows.map(w => ({
index: w.windowIndex,
name: w.windowName,
active: w.active,
})),
}));
return {
success: true,
sessionCount: sessions.length,
sessions: sessionDetails,
totalWindows: sessions.reduce((sum, s) => sum + s.windows.length, 0),
timestamp: new Date().toISOString(),
};
}
catch (error) {
throw new Error(`Failed to list sessions: ${error.message}`);
}
}
async healthCheck(context, itemIndex, bridge) {
const targetSessions = context.getNodeParameter('targetSessions', itemIndex, '');
const responseTimeout = context.getNodeParameter('responseTimeout', itemIndex, 10);
try {
const sessions = await bridge.getTmuxSessions();
const sessionsToCheck = targetSessions
? sessions.filter(s => targetSessions.split(',').includes(s.name.trim()))
: sessions;
const healthResults = [];
for (const session of sessionsToCheck) {
for (const window of session.windows) {
const pingMessage = `HEALTH CHECK: Please respond with "ACK" if you are operational.`;
await bridge.sendClaudeMessage(`${session.name}:${window.windowIndex}`, pingMessage);
}
}
await new Promise(resolve => setTimeout(resolve, responseTimeout * 1000));
for (const session of sessionsToCheck) {
for (const window of session.windows) {
const output = await bridge.captureWindowContent(session.name, window.windowIndex, 20);
let lastLines;
try {
if (typeof output === 'string' && output.length > 0) {
lastLines = output.split('\n').slice(-10).join('\n');
}
else {
lastLines = output ? `Health data in unexpected format: ${typeof output}` : 'No health data available';
}
}
catch (error) {
lastLines = `Error processing health data: ${error.message}`;
}
const isHealthy = lastLines.includes('ACK') || lastLines.includes('operational');
const hasErrors = /error|exception|failed/i.test(lastLines);
healthResults.push({
session: session.name,
window: window.windowName,
windowIndex: window.windowIndex,
healthy: isHealthy,
hasErrors,
responded: lastLines.includes('HEALTH CHECK'),
lastActivity: lastLines.substring(0, 200),
});
}
}
const healthySummary = {
totalChecked: healthResults.length,
healthy: healthResults.filter(r => r.healthy).length,
unhealthy: healthResults.filter(r => !r.healthy).length,
errors: healthResults.filter(r => r.hasErrors).length,
};
return {
success: true,
summary: healthySummary,
details: healthResults,
timestamp: new Date().toISOString(),
};
}
catch (error) {
throw new Error(`Health check failed: ${error.message}`);
}
}
async collectLogs(context, itemIndex, bridge) {
const targetSessions = context.getNodeParameter('targetSessions', itemIndex, '');
const linesPerWindow = context.getNodeParameter('linesPerWindow', itemIndex, 100);
const saveToFile = context.getNodeParameter('saveToFile', itemIndex, false);
const outputDirectory = context.getNodeParameter('outputDirectory', itemIndex, '/tmp/tmux-logs');
try {
const sessions = await bridge.getTmuxSessions();
const sessionsToLog = targetSessions
? sessions.filter(s => targetSessions.split(',').includes(s.name.trim()))
: sessions;
const logs = [];
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
for (const session of sessionsToLog) {
for (const window of session.windows) {
const content = await bridge.captureWindowContent(session.name, window.windowIndex, linesPerWindow);
const logEntry = {
session: session.name,
window: window.windowName,
windowIndex: window.windowIndex,
timestamp: new Date().toISOString(),
content,
};
logs.push(logEntry);
if (saveToFile) {
const path = require('path');
try {
await fs_1.promises.access(outputDirectory);
}
catch {
await fs_1.promises.mkdir(outputDirectory, { recursive: true });
}
const filename = `${session.name}_${window.windowName}_${timestamp}.log`;
const filepath = path.join(outputDirectory, filename);
await fs_1.promises.writeFile(filepath, content);
logEntry['savedTo'] = filepath;
}
}
}
return {
success: true,
logsCollected: logs.length,
logs: logs.map(l => ({
...l,
content: l.content.substring(0, 500) + '...',
})),
savedToFiles: saveToFile,
outputDirectory: saveToFile ? outputDirectory : null,
timestamp: new Date().toISOString(),
};
}
catch (error) {
throw new Error(`Failed to collect logs: ${error.message}`);
}
}
async detectBlockers(context, itemIndex, bridge) {
const targetSessions = context.getNodeParameter('targetSessions', itemIndex, '');
const inactivityThreshold = context.getNodeParameter('inactivityThreshold', itemIndex, 300);
const errorKeywords = context.getNodeParameter('errorKeywords', itemIndex, 'error,failed,exception,blocked,stuck,waiting');
try {
const sessions = await bridge.getTmuxSessions();
const sessionsToCheck = targetSessions
? sessions.filter(s => targetSessions.split(',').includes(s.name.trim()))
: sessions;
const keywords = errorKeywords.split(',').map(k => k.trim());
const blockers = [];
for (const session of sessionsToCheck) {
for (const window of session.windows) {
const content = await bridge.captureWindowContent(session.name, window.windowIndex, 100);
let lines = [];
let recentLines = [];
try {
if (typeof content === 'string' && content.length > 0) {
lines = content.split('\n');
recentLines = lines.slice(-20);
}
else if (content) {
lines = [];
recentLines = [];
}
}
catch (error) {
lines = [];
recentLines = [];
}
const foundKeywords = [];
for (const keyword of keywords) {
if (recentLines.some(line => line.toLowerCase().includes(keyword.toLowerCase()))) {
foundKeywords.push(keyword);
}
}
const uniqueLines = new Set(recentLines.filter(l => l.trim()));
const repetitionRatio = uniqueLines.size / recentLines.length;
const isRepetitive = repetitionRatio < 0.3;
const waitingForInput = recentLines.some(line => /waiting|input|enter|continue|proceed|y\/n|yes\/no/i.test(line));
if (foundKeywords.length > 0 || isRepetitive || waitingForInput) {
blockers.push({
session: session.name,
window: window.windowName,
windowIndex: window.windowIndex,
blockerType: foundKeywords.length > 0 ? 'error' :
isRepetitive ? 'repetitive' : 'waiting',
foundKeywords,
isRepetitive,
waitingForInput,
context: recentLines.length > 0 ? recentLines.slice(-10).join('\n') : 'No context available',
suggestedAction: this.getSuggestedAction(foundKeywords, isRepetitive, waitingForInput),
});
}
}
}
return {
success: true,
blockersFound: blockers.length,
blockers,
checkedSessions: sessionsToCheck.length,
timestamp: new Date().toISOString(),
};
}
catch (error) {
throw new Error(`Failed to detect blockers: ${error.message}`);
}
}
async monitorSnapshot(context, bridge) {
try {
const snapshot = await bridge.createMonitoringSnapshot();
const status = await bridge.getAllWindowsStatus();
const insights = this.analyzeSnapshot(status);
return {
success: true,
snapshot,
status,
insights,
timestamp: new Date().toISOString(),
};
}
catch (error) {
throw new Error(`Failed to create monitoring snapshot: ${error.message}`);
}
}
async findWindows(context, itemIndex, bridge) {
const windowPattern = context.getNodeParameter('windowPattern', itemIndex);
try {
const matches = await bridge.findWindowByName(windowPattern);
const results = matches.map(([sessionName, windowIndex]) => ({
session: sessionName,
windowIndex,
target: `${sessionName}:${windowIndex}`,
}));
return {
success: true,
pattern: windowPattern,
matchCount: results.length,
matches: results,
timestamp: new Date().toISOString(),
};
}
catch (error) {
throw new Error(`Failed to find windows: ${error.message}`);
}
}
async activityReport(context, itemIndex, bridge) {
const targetSessions = context.getNodeParameter('targetSessions', itemIndex, '');
const reportPeriod = context.getNodeParameter('reportPeriod', itemIndex, 'hour');
try {
const sessions = await bridge.getTmuxSessions();
const sessionsToReport = targetSessions
? sessions.filter(s => targetSessions.split(',').includes(s.name.trim()))
: sessions;
const activityData = [];
for (const session of sessionsToReport) {
const sessionActivity = {
session: session.name,
attached: session.attached,
windows: [],
};
for (const window of session.windows) {
const content = await bridge.captureWindowContent(session.name, window.windowIndex, 200);
let lines = [];
try {
if (typeof content === 'string' && content.length > 0) {
lines = content.split('\n');
}
else if (content) {
lines = [];
}
}
catch (error) {
lines = [];
}
const nonEmptyLines = lines.filter(l => l.trim()).length;
const commandCount = lines.filter(l => l.startsWith('$') || l.startsWith('>')).length;
const errorCount = lines.filter(l => /error|exception|failed/i.test(l)).length;
sessionActivity.windows.push({
name: window.windowName,
index: window.windowIndex,
active: window.active,
activityLevel: nonEmptyLines,
commandsExecuted: commandCount,
errorsDetected: errorCount,
lastActivity: lines.length > 0 ? lines.slice(-5).join('\n') : 'No activity available',
});
}
activityData.push(sessionActivity);
}
const summary = {
totalSessions: activityData.length,
totalWindows: activityData.reduce((sum, s) => sum + s.windows.length, 0),
activeWindows: activityData.reduce((sum, s) => sum + s.windows.filter(w => w.active).length, 0),
totalCommands: activityData.reduce((sum, s) => sum + s.windows.reduce((wSum, w) => wSum + w.commandsExecuted, 0), 0),
totalErrors: activityData.reduce((sum, s) => sum + s.windows.reduce((wSum, w) => wSum + w.errorsDetected, 0), 0),
};
return {
success: true,
reportPeriod,
summary,
activityData,
timestamp: new Date().toISOString(),
};
}
catch (error) {
throw new Error(`Failed to generate activity report: ${error.message}`);
}
}
getSuggestedAction(keywords, isRepetitive, waitingForInput) {
if (keywords.includes('error') || keywords.includes('exception')) {
return 'Check error logs and restart agent if necessary';
}
if (keywords.includes('blocked') || keywords.includes('stuck')) {
return 'Agent may be blocked. Check dependencies or restart task';
}
if (waitingForInput) {
return 'Agent waiting for input. Provide required information or cancel operation';
}
if (isRepetitive) {
return 'Agent may be in a loop. Interrupt and provide new instructions';
}
return 'Monitor agent and provide assistance if needed';
}
async checkSubagentOpportunities(context, itemIndex, bridge) {
const targetSessions = context.getNodeParameter('targetSessions', itemIndex, '');
try {
const sessions = await bridge.getTmuxSessions();
const sessionsToCheck = targetSessions
? sessions.filter(s => targetSessions.split(',').includes(s.name.trim()))
: sessions;
const opportunities = [];
const subagentKeywords = [
'complex', 'multiple', 'stuck', 'debugging', 'overwhelmed',
'taking too long', 'need help', 'complicated', 'many tasks',
'parallel', 'simultaneously', 'at the same time'
];
for (const session of sessionsToCheck) {
for (const window of session.windows) {
const content = await bridge.captureWindowContent(session.name, window.windowIndex, 100);
let lines = [];
let recentLines = '';
try {
if (typeof content === 'string' && content.length > 0) {
lines = content.split('\n');
recentLines = lines.slice(-50).join(' ').toLowerCase();
}
else if (content) {
lines = [];
recentLines = `subagent opportunity data in unexpected format: ${typeof content}`.toLowerCase();
}
}
catch (error) {
lines = [];
recentLines = `error processing subagent opportunity data: ${error.message}`.toLowerCase();
}
const foundKeywords = subagentKeywords.filter(kw => recentLines.includes(kw));
const uniqueLines = new Set(lines.length > 0 ? lines.slice(-20).filter(l => l.trim()) : []);
const isRepetitive = lines.length > 0 && uniqueLines.size < 5;
const hasLongRunning = lines.length > 80 && !recentLines.includes('complete') && !recentLines.includes('done');
let agentType = 'general';
if (window.windowName.toLowerCase().includes('manager')) {
agentType = 'pm';
}
else if (window.windowName.toLowerCase().includes('developer') ||
window.windowName.toLowerCase().includes('engineer')) {
agentType = 'developer';
}
if (foundKeywords.length > 0 || isRepetitive || hasLongRunning) {
opportunities.push({
session: session.name,
window: window.windowName,
windowIndex: window.windowIndex,
target: `${session.name}:${window.windowIndex}`,
agentType,
indicators: {
keywords: foundKeywords,
isRepetitive,
hasLongRunning,
},
suggestedSubagents: this.getSuggestedSubagents(foundKeywords, agentType),
recommendedAction: `Send subagent suggestion to ${session.name}:${window.windowIndex}`,
});
}
}
}
return {
success: true,
opportunitiesFound: opportunities.length,
opportunities,
checkedSessions: sessionsToCheck.length,
recommendation: opportunities.length > 0
? 'Deploy subagent suggestions to identified agents to improve parallel execution'
: 'All agents appear to be working efficiently',
timestamp: new Date().toISOString(),
};
}
catch (error) {
throw new Error(`Failed to check subagent opportunities: ${error.message}`);
}
}
getSuggestedSubagents(keywords, agentType) {
const suggestions = [];
if (keywords.includes('debugging') || keywords.includes('stuck')) {
suggestions.push('debugger', 'error-detective');
}
if (keywords.includes('complex') || keywords.includes('complicated')) {
suggestions.push('research-analyst', 'search-specialist');
}
if (keywords.includes('multiple') || keywords.includes('parallel')) {
if (agentType === 'pm') {
suggestions.push('developer', 'qa-expert', 'technical-writer');
}
else {
suggestions.push('test-automator', 'code-reviewer');
}
}
if (keywords.includes('overwhelmed') || keywords.includes('taking too long')) {
suggestions.push('fullstack-developer', 'performance-engineer');
}
if (suggestions.length === 0) {
if (agentType === 'pm') {
suggestions.push('developer', 'qa-expert');
}
else if (agentType === 'developer') {
suggestions.push('debugger', 'test-automator');
}
else {
suggestions.push('research-analyst', 'assistant');
}
}
return [...new Set(suggestions)];
}
analyzeSnapshot(status) {
const insights = {
totalSessions: status.sessions?.length || 0,
attachedSessions: status.sessions?.filter(s => s.attached).length || 0,
totalWindows: 0,
activeWindows: 0,
recommendations: [],
};
if (status.sessions) {
for (const session of status.sessions) {
insights.totalWindows += session.windows?.length || 0;
insights.activeWindows += session.windows?.filter(w => w.active).length || 0;
}
}
if (insights.totalSessions > 10) {
insights.recommendations.push('High number of sessions. Consider consolidating or terminating idle sessions.');
}
if (insights.attachedSessions === 0 && insights.totalSessions > 0) {
insights.recommendations.push('No attached sessions. Agents running in background only.');
}
if (insights.totalWindows > 50) {
insights.recommendations.push('Large number of windows. Review and close unnecessary windows.');
}
return insights;
}
}
exports.TmuxAgentMonitor = TmuxAgentMonitor;
//# sourceMappingURL=TmuxAgentMonitor.node.js.map