@commit451/salamander
Version:
Never be AFK
130 lines • 6.82 kB
JavaScript
import chalk from 'chalk';
import { RunnerService } from './runner.js';
import { CommandExecutor } from './executor.js';
import { generateDeviceId } from '../utils/device.js';
import { EncryptionService } from '../utils/encryption.js';
import { loadAuth, saveAuth } from '../utils/storage.js';
export class CommandListener {
activeListeners = new Map();
isShuttingDown = false;
async startListening(runner) {
if (this.activeListeners.has(runner.id)) {
console.log(chalk.yellow(`Already listening to runner "${runner.name}"`));
return;
}
// Verify this is a local runner
const deviceId = generateDeviceId();
if (runner.machineId !== deviceId) {
throw new Error('Cannot listen to non-local runner');
}
// Generate or load encryption code
let encryptionCode;
const storedAuth = await loadAuth();
if (storedAuth?.encryptionCode) {
encryptionCode = storedAuth.encryptionCode;
}
else {
// Generate new random encryption code
encryptionCode = EncryptionService.generateRandomSecret();
// Save to storage
const updatedAuth = { ...storedAuth, encryptionCode };
await saveAuth(updatedAuth);
}
console.log(chalk.blue(`👂 Started listening for commands on runner "${runner.name}"`));
console.log(chalk.gray(` Directory: ${runner.directory}`));
console.log(chalk.gray(` Type: ${runner.runnerType}`));
console.log('');
// Display encryption code prominently
console.log(chalk.bgGreen.black(' ENCRYPTION CODE '));
console.log(chalk.green(` ${encryptionCode}`));
console.log(chalk.gray(' Enter this code in your Salamander mobile app to securely'));
console.log(chalk.gray(' connect to this runner. Your messages and commands are end to end encrypted'));
console.log('');
console.log(chalk.yellow('⚠️ SECURITY WARNING'));
console.log(chalk.yellow(' Claude commands will be executed with --dangerously-skip-permissions'));
console.log(chalk.yellow(' This bypasses normal permission checks for file access and operations'));
console.log(chalk.yellow(' Be careful about what directories you run this in and what commands you execute'));
console.log('');
console.log(chalk.gray(` Press Ctrl+C to stop`));
console.log(chalk.gray(` Keep in mind, if your machine goes to sleep, it won't process commands`));
const unsubscribe = RunnerService.listenToRunner(runner.id, async (updatedRunner) => {
if (!updatedRunner || this.isShuttingDown)
return;
if (updatedRunner.pendingCommand) {
// Decrypt the pending command
const decryptResult = EncryptionService.decrypt(updatedRunner.pendingCommand, encryptionCode);
if (!decryptResult.success) {
console.log(chalk.red(`❌ Failed to decrypt command: ${updatedRunner.pendingCommand}`));
// Clear the bad command
await RunnerService.clearPendingCommand(runner.id);
return;
}
const decryptedCommand = decryptResult.decrypted;
console.log(chalk.cyan(`📨 Received command: ${decryptedCommand}`));
try {
// Clear the pending command immediately
await RunnerService.clearPendingCommand(runner.id);
// Execute the decrypted command
const result = await CommandExecutor.executeCommand(updatedRunner, decryptedCommand);
// Encrypt the result
const encryptResult = EncryptionService.encrypt(result.output, encryptionCode);
const encryptedOutput = encryptResult.success ? encryptResult.encrypted : result.output;
// Create assistant message for the result
await RunnerService.createMessage(runner.id, {
content: encryptedOutput,
senderName: runner.name,
type: 'runner',
});
// Update the runner with the encrypted result
await RunnerService.updateRunnerAfterCommand(runner.id, encryptedOutput);
console.log(chalk.blue(`💌 Message sent`));
console.log(chalk.gray('Waiting for next command...'));
}
catch (error) {
console.error(chalk.red(`❌ Error processing command: ${error.message}`));
const errorMessage = `Error executing command: ${error.message}`;
// Encrypt the error message
const encryptResult = EncryptionService.encrypt(errorMessage, encryptionCode);
const encryptedError = encryptResult.success ? encryptResult.encrypted : errorMessage;
try {
// Create assistant message for the error
await RunnerService.createMessage(runner.id, {
content: encryptedError,
senderName: runner.name,
type: 'runner',
});
await RunnerService.updateRunnerAfterCommand(runner.id, encryptedError);
}
catch (updateError) {
console.error(chalk.red('Failed to update runner with error result'));
}
console.log(chalk.gray('Waiting for next command...'));
}
}
});
this.activeListeners.set(runner.id, unsubscribe);
// Set up graceful shutdown
const cleanup = async () => {
this.isShuttingDown = true;
await this.stopListening(runner.id);
console.log(chalk.blue(`👋 Stopped runner "${runner.name}"`));
process.exit(0);
};
// Remove any existing listeners for these signals to avoid duplicates
process.removeAllListeners('SIGINT');
process.removeAllListeners('SIGTERM');
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
// Also handle uncaught exceptions
process.on('uncaughtException', cleanup);
process.on('unhandledRejection', cleanup);
}
async stopListening(runnerId) {
const unsubscribe = this.activeListeners.get(runnerId);
if (unsubscribe) {
unsubscribe();
this.activeListeners.delete(runnerId);
}
}
}
//# sourceMappingURL=command-listener.js.map