import { exec } from 'child_process';
import { platform, tmpdir } from 'os';
import { existsSync, writeFileSync } from 'fs';
import { join } from 'path';
import type { CommandInjection, InterClaudeMessage } from './types.js';
export class CommandInjector {
private platform: string;
constructor() {
this.platform = platform();
}
// Simple file-based command notification that works everywhere
async injectCommand(targetPid: number, command: string, options: any = {}): Promise<boolean> {
try {
// Use existing imports from the top of file
const commandsDir = join(tmpdir(), 'claude-senator', 'commands');
const commandFile = join(commandsDir, `${targetPid}-${Date.now()}.json`);
const commandData = {
targetPid,
command,
from: process.pid,
timestamp: Date.now(),
type: 'command_injection'
};
writeFileSync(commandFile, JSON.stringify(commandData, null, 2));
console.log(`📨 Command written for Claude ${targetPid}: ${command}`);
return true;
} catch (error) {
console.error('[Claude Senator] File injection failed:', error);
return false;
}
}
private async sendSocketCommand(targetPid: number, command: string, options: any): Promise<boolean> {
try {
const { SessionManager } = await import('./session.js');
// This would be injected by the main coordinator
// For now, return false to trigger platform injection
return false;
} catch {
return false;
}
}
private async platformInject(command: string, targetPid?: number): Promise<boolean> {
return new Promise((resolve) => {
try {
switch (this.platform) {
case 'darwin':
this.macOSInject(command, resolve, targetPid);
break;
case 'linux':
this.linuxInject(command, resolve);
break;
case 'win32':
this.windowsInject(command, resolve);
break;
default:
resolve(false);
}
} catch {
resolve(false);
}
});
}
private macOSInject(command: string, callback: (success: boolean) => void, targetPid?: number): void {
// If we have a target PID, try to find the specific terminal first
if (targetPid) {
this.findTerminalForPid(targetPid, (terminal) => {
if (terminal) {
this.injectToSpecificTerminal(command, terminal, callback);
return;
}
// Fallback to generic injection
this.genericMacOSInject(command, callback);
});
} else {
this.genericMacOSInject(command, callback);
}
}
private findTerminalForPid(targetPid: number, callback: (terminal: string | null) => void): void {
// Get the TTY for the target process
exec(`ps -p ${targetPid} -o tty=`, (error, stdout) => {
if (error || !stdout.trim()) {
callback(null);
return;
}
const tty = stdout.trim();
console.error(`[Claude Senator] Target PID ${targetPid} is on TTY: ${tty}`);
callback(tty);
});
}
private injectToSpecificTerminal(command: string, tty: string, callback: (success: boolean) => void): void {
// Try to use the TTY information to target the specific terminal
// For now, fall back to generic injection but with TTY info logged
console.error(`[Claude Senator] Targeting terminal on TTY: ${tty}`);
this.genericMacOSInject(command, callback);
}
private genericMacOSInject(command: string, callback: (success: boolean) => void): void {
// Method 1: AppleScript to Terminal
const script = `
tell application "Terminal"
if (count of windows) = 0 then
do script "${command.replace(/"/g, '\\"')}"
else
do script "${command.replace(/"/g, '\\"')}" in front window
end if
activate
end tell
`;
exec(`osascript -e '${script}'`, (error) => {
if (!error) {
callback(true);
return;
}
// Method 2: Try iTerm2
const itermScript = `
tell application "iTerm"
if (count of windows) = 0 then
create window with default profile
end if
tell current session of current window
write text "${command.replace(/"/g, '\\"')}"
end tell
activate
end tell
`;
exec(`osascript -e '${itermScript}'`, (error2) => {
callback(!error2);
});
});
}
private linuxInject(command: string, callback: (success: boolean) => void): void {
// Method 1: Try tmux
exec('tmux list-sessions', (error, stdout) => {
if (!error && stdout) {
exec(`tmux send-keys "${command}" Enter`, (tmuxError) => {
if (!tmuxError) {
callback(true);
return;
}
this.linuxFallback(command, callback);
});
} else {
this.linuxFallback(command, callback);
}
});
}
private linuxFallback(command: string, callback: (success: boolean) => void): void {
// Method 2: Try xdotool
exec('which xdotool', (error) => {
if (!error) {
exec(`xdotool type "${command}" && xdotool key Return`, (xdoError) => {
if (!xdoError) {
callback(true);
return;
}
this.clipboardFallback(command, callback);
});
} else {
this.clipboardFallback(command, callback);
}
});
}
private windowsInject(command: string, callback: (success: boolean) => void): void {
// Windows: PowerShell to send keys
const ps1Script = `
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait("${command.replace(/"/g, '""')}")
[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")
`;
exec(`powershell -Command "${ps1Script}"`, (error) => {
if (!error) {
callback(true);
} else {
this.clipboardFallback(command, callback);
}
});
}
private clipboardFallback(command: string, callback: (success: boolean) => void): void {
// Universal fallback - copy to clipboard
let clipCmd = '';
switch (this.platform) {
case 'darwin':
clipCmd = `echo "${command}" | pbcopy`;
break;
case 'linux':
clipCmd = `echo "${command}" | xclip -selection clipboard`;
break;
case 'win32':
clipCmd = `echo "${command}" | clip`;
break;
default:
callback(false);
return;
}
exec(clipCmd, (error) => {
if (!error) {
console.log(`📋 [Claude Senator] Command copied to clipboard: ${command}`);
}
callback(!error);
});
}
// Send interactive choice menu
async sendChoiceMenu(targetPid: number, context: string, choices: string[]): Promise<boolean> {
const menu = this.formatChoiceMenu(context, choices);
return this.injectCommand(targetPid, menu);
}
private formatChoiceMenu(context: string, choices: string[]): string {
let menu = `📋 Handoff: ${context}\n`;
choices.forEach((choice, index) => {
menu += `[${index + 1}] ${choice}\n`;
});
menu += 'Choose an option:';
return menu;
}
// Quick status injection
async sendStatus(targetPid: number, status: string): Promise<boolean> {
const message = `📊 Status update: ${status}`;
return this.injectCommand(targetPid, message);
}
// Urgent message injection
async sendUrgent(targetPid: number, message: string): Promise<boolean> {
const urgent = `🚨 URGENT: ${message}`;
return this.injectCommand(targetPid, urgent);
}
// Context transfer - send full conversation context
async transferContext(targetPid: number, conversationData: any): Promise<boolean> {
try {
// Compress context to essential info only
const summary = this.compressContext(conversationData);
const transfer = `🔄 Context Transfer:\n${summary}\n\nContinue from here:`;
return this.injectCommand(targetPid, transfer);
} catch (error) {
console.error('[Claude Senator] Context transfer failed:', error);
return false;
}
}
private compressContext(data: any): string {
try {
if (typeof data === 'string') {
return data.slice(0, 200); // Limit length
}
if (data && typeof data === 'object') {
const keys = ['task', 'status', 'files', 'errors', 'summary'];
const relevant = keys
.filter(key => data[key])
.map(key => `${key}: ${String(data[key]).slice(0, 50)}`)
.join(', ');
return relevant || 'Context transfer';
}
return String(data).slice(0, 200);
} catch {
return 'Context transfer';
}
}
}