add configuration profiles with per-invocation --profile flag
- Add SystemPrompt field to HushConfig (empty = built-in default) - Refactor ConfigManager: extract ApplyTomlFields, add LoadWithProfile(), ListProfiles(), GetProfilePath(), EnsureProfilesDirExists(); remove HUSH_PROFILE env-var logic (profiles are now resolved by the CLI) - Extend socket protocol: action commands (START/STOP/TOGGLE/ABORT) now carry a [4-byte LE length][optional HushConfig JSON] payload so the CLI can pass a per-invocation config override without restarting the daemon - Add GENERATE_PROFILE (cmd 7) socket command: CLI sends a description, daemon calls the LLM and returns a generated system prompt - Orchestrator: StopAndProcessAsync accepts optional HushConfig override; ProcessWithLlmAsync uses proper system/user chat roles and respects config.SystemPrompt; add GenerateProfilePromptAsync - Split CompleteTextAsync signature to (systemPrompt, userMessage, model) across ITextStreamingProvider, GroqProvider, FireworksProvider - Add --profile/-p flag to hush toggle and hush stop - Add hush profiles subcommand: list, get, new (manual or AI-generated), edit
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Hush.Config;
|
||||
using Hush.Daemon;
|
||||
|
||||
namespace Hush.Cli;
|
||||
@@ -15,7 +17,7 @@ public class SocketClient : IAsyncDisposable
|
||||
var runtimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
|
||||
var baseDir = string.IsNullOrEmpty(runtimeDir) ? Path.GetTempPath() : runtimeDir;
|
||||
var socketPath = Path.Combine(baseDir, "hush.sock");
|
||||
|
||||
|
||||
_endPoint = new UnixDomainSocketEndPoint(socketPath);
|
||||
_socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
|
||||
}
|
||||
@@ -26,9 +28,58 @@ public class SocketClient : IAsyncDisposable
|
||||
await _socket.ConnectAsync(_endPoint, cts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a command with no config payload.
|
||||
/// Action commands (START/STOP/TOGGLE/ABORT) always include a 4-byte zero length prefix
|
||||
/// so the daemon can read the same framing unconditionally.
|
||||
/// </summary>
|
||||
public async Task SendCommandAsync(byte command)
|
||||
{
|
||||
await _socket.SendAsync(new[] { command }, SocketFlags.None);
|
||||
if (IsActionCommand(command))
|
||||
{
|
||||
// [cmd][4 zero bytes] — signals no config override
|
||||
var frame = new byte[5];
|
||||
frame[0] = command;
|
||||
await _socket.SendAsync(frame, SocketFlags.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _socket.SendAsync(new[] { command }, SocketFlags.None);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an action command with a HushConfig override payload.
|
||||
/// Format: [1 byte cmd][4-byte LE length][N bytes JSON]
|
||||
/// </summary>
|
||||
public async Task SendCommandWithConfigAsync(byte command, HushConfig config)
|
||||
{
|
||||
var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(config, HushConfigContext.Default.HushConfig);
|
||||
var lenBytes = BitConverter.GetBytes(jsonBytes.Length);
|
||||
|
||||
var frame = new byte[1 + 4 + jsonBytes.Length];
|
||||
frame[0] = command;
|
||||
lenBytes.CopyTo(frame, 1);
|
||||
jsonBytes.CopyTo(frame, 5);
|
||||
|
||||
await _socket.SendAsync(frame, SocketFlags.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a request with a typed JSON payload (e.g. GENERATE_PROFILE).
|
||||
/// Format: [1 byte cmd][4-byte LE length][N bytes JSON]
|
||||
/// </summary>
|
||||
public async Task SendRequestAsync<TRequest>(byte command, TRequest payload, JsonTypeInfo<TRequest> typeInfo)
|
||||
{
|
||||
var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(payload, typeInfo);
|
||||
var lenBytes = BitConverter.GetBytes(jsonBytes.Length);
|
||||
|
||||
var frame = new byte[1 + 4 + jsonBytes.Length];
|
||||
frame[0] = command;
|
||||
lenBytes.CopyTo(frame, 1);
|
||||
jsonBytes.CopyTo(frame, 5);
|
||||
|
||||
await _socket.SendAsync(frame, SocketFlags.None);
|
||||
}
|
||||
|
||||
public async Task<T?> ReceiveJsonAsync<T>(TimeSpan timeout)
|
||||
@@ -36,10 +87,10 @@ public class SocketClient : IAsyncDisposable
|
||||
var cts = new CancellationTokenSource(timeout);
|
||||
var buffer = new byte[4096];
|
||||
var bytesRead = await _socket.ReceiveAsync(buffer, SocketFlags.None, cts.Token);
|
||||
|
||||
|
||||
if (bytesRead == 0)
|
||||
return default;
|
||||
|
||||
|
||||
var json = Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
||||
return (T?)JsonSerializer.Deserialize(json, typeof(T), DaemonJsonContext.Default);
|
||||
}
|
||||
@@ -61,4 +112,8 @@ public class SocketClient : IAsyncDisposable
|
||||
_socket.Dispose();
|
||||
await ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private static bool IsActionCommand(byte command) =>
|
||||
command is DaemonProtocol.START or DaemonProtocol.STOP
|
||||
or DaemonProtocol.ABORT or DaemonProtocol.TOGGLE;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user