using System; using System.Diagnostics; using System.IO; using System.Net.Sockets; using System.Threading.Tasks; using Toak.Configuration; using Toak.Api; using Toak.Core.Interfaces; using Toak.Audio; using Toak.IO; namespace Toak.Core; public static class DaemonService { public static string GetSocketPath() { return Constants.Paths.GetSocketPath(); } private static FileStream? _lockFile; public static async Task StartAsync(bool verbose) { var lockPath = Constants.Paths.DaemonLockFile; try { Directory.CreateDirectory(Path.GetDirectoryName(lockPath)!); _lockFile = new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); } catch (IOException) { Console.WriteLine("Toak daemon is already running."); return; } Logger.Verbose = verbose; var socketPath = GetSocketPath(); if (File.Exists(socketPath)) { try { File.Delete(socketPath); } catch { } } var configManager = new ConfigManager(); var config = configManager.LoadConfig(); if (string.IsNullOrWhiteSpace(config.GroqApiKey)) { Console.WriteLine("Groq API Key is not configured. Run 'toak onboard'."); return; } var stateTracker = new StateTracker(); var notifications = new Notifications(); var speechClient = new OpenAiCompatibleClient(config.GroqApiKey); ILlmClient llmClient; if (config.LlmProvider == "together") { llmClient = new OpenAiCompatibleClient(config.TogetherApiKey, "https://api.together.xyz/v1/", config.ReasoningEffort); } else { llmClient = new OpenAiCompatibleClient(config.GroqApiKey, "https://api.groq.com/openai/v1/", config.ReasoningEffort); } IAudioRecorder recorder = config.AudioBackend == "ffmpeg" ? new FfmpegAudioRecorder(stateTracker, notifications) : new AudioRecorder(stateTracker, notifications); var orchestrator = new TranscriptionOrchestrator( speechClient, llmClient, configManager, recorder, notifications, new TextInjector(notifications), new HistoryManager(), new ClipboardManager(notifications), stateTracker ); using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); var endPoint = new UnixDomainSocketEndPoint(socketPath); try { socket.Bind(endPoint); socket.Listen(10); Logger.LogDebug($"Daemon listening on {socketPath}"); Console.WriteLine($"Toak daemon started, listening on {socketPath}"); while (true) { var client = await socket.AcceptAsync(); _ = Task.Run(() => HandleClientAsync(client, orchestrator, stateTracker)); } } catch (Exception ex) { Logger.LogDebug($"Daemon error: {ex.Message}"); } finally { if (File.Exists(socketPath)) { File.Delete(socketPath); } } } private static async Task HandleClientAsync(Socket client, ITranscriptionOrchestrator orchestrator, IRecordingStateTracker stateTracker) { try { var buffer = new byte[3]; int bytesRead = await client.ReceiveAsync(buffer, SocketFlags.None); if (bytesRead > 0) { byte cmd = buffer[0]; bool pipeToStdout = bytesRead > 1 && buffer[1] == 1; bool copyToClipboard = bytesRead > 2 && buffer[2] == 1; if (cmd == 1) // START { await orchestrator.ProcessStartRecordingAsync(); } else if (cmd == 2) // STOP { await orchestrator.ProcessStopRecordingAsync(client, pipeToStdout, copyToClipboard); } else if (cmd == 3) // ABORT { orchestrator.ProcessAbortAsync(); } else if (cmd == 4) // TOGGLE { if (stateTracker.IsRecording()) await orchestrator.ProcessStopRecordingAsync(client, pipeToStdout, copyToClipboard); else await orchestrator.ProcessStartRecordingAsync(); } } } catch (Exception ex) { Logger.LogDebug($"HandleClient error: {ex.Message}"); } finally { client.Close(); } } }