UNPKG

@commit451/salamander

Version:

Never be AFK

130 lines 6.82 kB
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