Fix MCP server socket path for parallel dev environments

Pass HUMANLAYER_DAEMON_SOCKET environment variable to MCP servers launched
by the daemon. This ensures MCP servers connect back to the correct daemon
instance (dev vs nightly) instead of always using the default socket path.

- Update session Manager to accept and store socket path
- Add HUMANLAYER_DAEMON_SOCKET to MCP server environment variables
- Update MCP server to read socket path from environment
- Update all tests to pass socket path parameter
This commit is contained in:
dexhorthy
2025-07-17 20:35:18 -07:00
parent e23d3f85c7
commit e136b875e7
19 changed files with 381 additions and 31 deletions

2
.gitignore vendored
View File

@@ -180,3 +180,5 @@ cython_debug/
.idea/
hlyr/blah.txt
hld/hld-dev
hld/hld-nightly

172
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,172 @@
# Development Guide
This guide covers development workflows and tools for the HumanLayer repository.
## Parallel Development Environments
The HumanLayer development setup supports running parallel daemon/WUI instances to prevent development work from disrupting active Claude sessions. You can run a stable "nightly" environment for regular work alongside a "dev" environment for testing changes.
### Quick Start
```bash
# Start nightly (stable) environment
make daemon-nightly
make wui-nightly
# Start dev environment (in another terminal)
make daemon-dev
make wui-dev
# Launch Claude Code with specific daemon
npx humanlayer launch "implement feature X" --daemon-socket ~/.humanlayer/daemon-dev.sock
```
### Environment Overview
| Component | Nightly (Stable) | Dev (Testing) |
|-----------|------------------|---------------|
| Daemon Binary | `hld/hld-nightly` | `hld/hld-dev` |
| Socket Path | `~/.humanlayer/daemon.sock` | `~/.humanlayer/daemon-dev.sock` |
| Database | `~/.humanlayer/daemon.db` | `~/.humanlayer/dev/daemon-TIMESTAMP.db` |
| Log Files | `daemon-nightly-*.log` | `daemon-dev-*.log` |
| WUI | Installed in `~/Applications` | Running in dev mode |
### Available Commands
#### Nightly (Stable) Environment
```bash
make daemon-nightly-build # Build nightly daemon binary
make daemon-nightly # Build and run nightly daemon
make wui-nightly-build # Build nightly WUI
make wui-nightly # Build, install, and open nightly WUI
```
#### Dev Environment
```bash
make daemon-dev-build # Build dev daemon binary
make daemon-dev # Build and run dev daemon with fresh DB copy
make wui-dev # Run WUI in dev mode connected to dev daemon
make copy-db-to-dev # Manually copy production DB to timestamped dev DB
make cleanup-dev # Clean up dev DBs and logs older than 10 days
```
#### Status and Utilities
```bash
make dev-status # Show current dev environment status
```
### Claude Code Integration
**Note**: The `hlyr` package was updated on July 18th and needs to be reinstalled to use the latest flow:
```bash
cd hlyr && npm i -g .
```
The `npx humanlayer launch` command supports custom daemon sockets through multiple methods:
#### 1. Command-line Flag
```bash
npx humanlayer launch "test my implementation" --daemon-socket ~/.humanlayer/daemon-dev.sock
```
#### 2. Environment Variable
```bash
HUMANLAYER_DAEMON_SOCKET=~/.humanlayer/daemon-dev.sock npx humanlayer launch "test feature"
```
#### 3. Configuration File
Add to your `humanlayer.json`:
```json
{
"daemon_socket": "~/.humanlayer/daemon-dev.sock"
}
```
### Typical Development Workflow
1. **Start your nightly environment** (once per day):
```bash
make daemon-nightly
make wui-nightly
```
2. **Work on a feature branch**:
```bash
git checkout -b feature/my-feature
# Make your changes
```
3. **Test your changes** without disrupting nightly:
```bash
# Terminal 1: Start dev daemon
make daemon-dev
# Terminal 2: Test with Claude Code
npx humanlayer launch "test my feature" --daemon-socket ~/.humanlayer/daemon-dev.sock
# Or use dev WUI
make wui-dev
```
4. **Clean up old dev artifacts** (weekly):
```bash
make cleanup-dev
```
### Key Features
- **Isolated Databases**: Each dev daemon run gets a fresh copy of your production database
- **Separate Sockets**: Dev and nightly daemons use different Unix sockets
- **Version Identification**: Dev daemon shows version as "dev" in WUI
- **Automatic Logging**: All daemon runs are logged with timestamps
- **No Cross-Contamination**: Changes in dev environment don't affect your stable nightly setup
### Environment Variables
Both daemon and WUI respect these environment variables:
- `HUMANLAYER_DAEMON_SOCKET`: Path to daemon socket (default: `~/.humanlayer/daemon.sock`)
- `HUMANLAYER_DATABASE_PATH`: Path to SQLite database (daemon only)
- `HUMANLAYER_DAEMON_VERSION_OVERRIDE`: Custom version string (daemon only)
### Troubleshooting
**Both daemons won't start**: Check if old processes are running:
```bash
ps aux | grep hld | grep -v grep
```
**WUI can't connect**: Verify the socket path matches between WUI and daemon:
```bash
# Check which sockets exist
ls -la ~/.humanlayer/*.sock
```
**Database issues**: Each dev daemon run creates a new timestamped database:
```bash
# List all dev databases
ls -la ~/.humanlayer/dev/
```
## Other Development Commands
### Building and Testing
```bash
make setup # Resolve dependencies across monorepo
make check-test # Run all checks and tests
make check # Run linting and type checking
make test # Run all test suites
```
### Python Development
```bash
make check-py # Python linting and type checking
make test-py # Python tests
```
### TypeScript Development
Check individual `package.json` files for specific commands, as package managers and test frameworks vary across projects.
### Go Development
Check `go.mod` for Go version requirements and look for `Makefile` in each Go project directory.

View File

@@ -476,3 +476,96 @@ daemon:
@mkdir -p ~/.humanlayer/logs
echo "$(logfileprefix) starting daemon in $(shell pwd)" > ~/.humanlayer/logs/daemon-$(logfileprefix).log
cd hlyr && npm run build && ./dist/bin/hld 2>&1 | tee -a ~/.humanlayer/logs/daemon-$(logfileprefix).log
# Build nightly daemon binary
.PHONY: daemon-nightly-build
daemon-nightly-build:
cd hld && go build -o hld-nightly ./cmd/hld
@echo "Built nightly daemon binary: hld/hld-nightly"
# Run nightly daemon
.PHONY: daemon-nightly
daemon-nightly: daemon-nightly-build
@mkdir -p ~/.humanlayer/logs
echo "$$(date +%Y-%m-%d-%H-%M-%S) starting nightly daemon in $$(pwd)" > ~/.humanlayer/logs/daemon-nightly-$$(date +%Y-%m-%d-%H-%M-%S).log
cd hld && ./hld-nightly 2>&1 | tee -a ~/.humanlayer/logs/daemon-nightly-$$(date +%Y-%m-%d-%H-%M-%S).log
# Build and install nightly WUI
.PHONY: wui-nightly-build
wui-nightly-build:
cd humanlayer-wui && bun run tauri build
@echo "Build complete. Installing to ~/Applications..."
cp -r humanlayer-wui/src-tauri/target/release/bundle/macos/humanlayer-wui.app ~/Applications/
@echo "Installed WUI nightly to ~/Applications/humanlayer-wui.app"
# Open nightly WUI
.PHONY: wui-nightly
wui-nightly: wui-nightly-build
@echo "Opening WUI nightly..."
open ~/Applications/humanlayer-wui.app
# Copy production database to timestamped dev database
.PHONY: copy-db-to-dev
copy-db-to-dev:
@mkdir -p ~/.humanlayer/dev
$(eval TIMESTAMP := $(shell date +%Y-%m-%d-%H-%M-%S))
$(eval DEV_DB := ~/.humanlayer/dev/daemon-$(TIMESTAMP).db)
@if [ -f ~/.humanlayer/daemon.db ]; then \
cp ~/.humanlayer/daemon.db $(DEV_DB); \
echo "Copied production database to: $(DEV_DB)" >&2; \
echo "$(DEV_DB)"; \
else \
echo "Error: Production database not found at ~/.humanlayer/daemon.db" >&2; \
exit 1; \
fi
# Clean up dev databases and logs older than 10 days
.PHONY: cleanup-dev
cleanup-dev:
@echo "Cleaning up dev artifacts older than 10 days..."
@# Clean old dev databases
@if [ -d ~/.humanlayer/dev ]; then \
find ~/.humanlayer/dev -name "daemon-*.db" -type f -mtime +10 -delete -print | sed 's/^/Deleted database: /'; \
fi
@# Clean old dev logs
@if [ -d ~/.humanlayer/logs ]; then \
find ~/.humanlayer/logs -name "*-dev-*.log" -type f -mtime +10 -delete -print | sed 's/^/Deleted log: /'; \
fi
@echo "Cleanup complete."
# Build dev daemon binary
.PHONY: daemon-dev-build
daemon-dev-build:
cd hld && go build -o hld-dev ./cmd/hld
@echo "Built dev daemon binary: hld/hld-dev"
# Run dev daemon with fresh database copy
.PHONY: daemon-dev
daemon-dev: daemon-dev-build
@mkdir -p ~/.humanlayer/logs
$(eval TIMESTAMP := $(shell date +%Y-%m-%d-%H-%M-%S))
$(eval DEV_DB := $(shell make -s copy-db-to-dev))
@echo "Starting dev daemon with database: $(DEV_DB)"
echo "$(TIMESTAMP) starting dev daemon in $$(pwd)" > ~/.humanlayer/logs/daemon-dev-$(TIMESTAMP).log
cd hld && HUMANLAYER_DATABASE_PATH=$(DEV_DB) HUMANLAYER_DAEMON_SOCKET=~/.humanlayer/daemon-dev.sock HUMANLAYER_DAEMON_VERSION_OVERRIDE=dev ./hld-dev 2>&1 | tee -a ~/.humanlayer/logs/daemon-dev-$(TIMESTAMP).log
# Run dev WUI with custom socket
.PHONY: wui-dev
wui-dev:
@mkdir -p ~/.humanlayer/logs
$(eval TIMESTAMP := $(shell date +%Y-%m-%d-%H-%M-%S))
echo "$(TIMESTAMP) starting dev wui in $$(pwd)" > ~/.humanlayer/logs/wui-dev-$(TIMESTAMP).log
cd humanlayer-wui && HUMANLAYER_DAEMON_SOCKET=~/.humanlayer/daemon-dev.sock bun run tauri dev 2>&1 | tee -a ~/.humanlayer/logs/wui-dev-$(TIMESTAMP).log
# Show current dev environment setup
.PHONY: dev-status
dev-status:
@echo "=== Development Environment Status ==="
@echo "Dev Socket: ~/.humanlayer/daemon-dev.sock"
@echo "Nightly Socket: ~/.humanlayer/daemon.sock"
@echo ""
@echo "Dev Databases:"
@ls -la ~/.humanlayer/dev/*.db 2>/dev/null || echo " No dev databases found"
@echo ""
@echo "Active daemons:"
@ps aux | grep -E "hld(-dev|-nightly)?$$" | grep -v grep || echo " No daemons running"

View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
@@ -96,6 +97,10 @@ func (c *Client) buildArgs(config SessionConfig) ([]string, error) {
if err != nil {
return nil, fmt.Errorf("failed to marshal MCP config: %w", err)
}
// Log MCP config for debugging
log.Printf("MCP config JSON: %s", string(mcpJSON))
// Create a temp file for MCP config
tmpFile, err := os.CreateTemp("", "mcp-config-*.json")
if err != nil {
@@ -107,6 +112,8 @@ func (c *Client) buildArgs(config SessionConfig) ([]string, error) {
return nil, fmt.Errorf("failed to write MCP config: %w", err)
}
_ = tmpFile.Close()
log.Printf("MCP config written to: %s", tmpFile.Name())
args = append(args, "--mcp-config", tmpFile.Name())
// Note: temp file will be cleaned up when process exits
@@ -153,6 +160,7 @@ func (c *Client) Launch(config SessionConfig) (*Session, error) {
return nil, err
}
log.Printf("Executing Claude command: %s %v", c.claudePath, args)
cmd := exec.Command(c.claudePath, args...)
// Set working directory if specified

View File

@@ -22,6 +22,9 @@ type Config struct {
// Logging configuration
LogLevel string `mapstructure:"log_level"`
// Version override for display purposes (e.g., "dev" for development instances)
VersionOverride string `mapstructure:"version_override"`
}
// Load loads configuration with priority: flags > env vars > config file > defaults
@@ -47,6 +50,7 @@ func Load() (*Config, error) {
_ = v.BindEnv("api_key", "HUMANLAYER_API_KEY")
_ = v.BindEnv("api_base_url", "HUMANLAYER_API_BASE_URL", "HUMANLAYER_API_BASE")
_ = v.BindEnv("log_level", "HUMANLAYER_LOG_LEVEL")
_ = v.BindEnv("version_override", "HUMANLAYER_DAEMON_VERSION_OVERRIDE")
// Set defaults
v.SetDefault("socket_path", "~/.humanlayer/daemon.sock")

View File

@@ -89,7 +89,7 @@ func New() (*Daemon, error) {
}
// Create session manager with store and config
sessionManager, err := session.NewManager(eventBus, conversationStore)
sessionManager, err := session.NewManager(eventBus, conversationStore, cfg.SocketPath)
if err != nil {
_ = conversationStore.Close()
return nil, fmt.Errorf("failed to create session manager: %w", err)
@@ -147,7 +147,11 @@ func (d *Daemon) Run(ctx context.Context) error {
}()
// Create and start RPC server
d.rpcServer = rpc.NewServer()
if d.config.VersionOverride != "" {
d.rpcServer = rpc.NewServerWithVersionOverride(d.config.VersionOverride)
} else {
d.rpcServer = rpc.NewServer()
}
// Mark orphaned sessions as failed (from previous daemon run)
if err := d.markOrphanedSessionsAsFailed(ctx); err != nil {

View File

@@ -37,7 +37,7 @@ func TestDaemonApprovalIntegration(t *testing.T) {
approvalManager := approval.NewManager(testStore, eventBus)
// Create session manager
sessionManager, err := session.NewManager(eventBus, testStore)
sessionManager, err := session.NewManager(eventBus, testStore, "")
if err != nil {
t.Fatalf("failed to create session manager: %v", err)
}

View File

@@ -30,7 +30,7 @@ func TestIntegrationContinueSession(t *testing.T) {
}
defer func() { _ = sqliteStore.Close() }()
sessionManager, err := session.NewManager(eventBus, sqliteStore)
sessionManager, err := session.NewManager(eventBus, sqliteStore, "")
if err != nil {
t.Fatalf("Failed to create session manager: %v", err)
}

View File

@@ -30,7 +30,7 @@ func TestIntegrationResumeDuringRunning(t *testing.T) {
}
defer func() { _ = sqliteStore.Close() }()
sessionManager, err := session.NewManager(eventBus, sqliteStore)
sessionManager, err := session.NewManager(eventBus, sqliteStore, "")
if err != nil {
t.Fatalf("Failed to create session manager: %v", err)
}

View File

@@ -41,7 +41,7 @@ func TestDaemonSessionStateIntegration(t *testing.T) {
approvalManager := approval.NewManager(testStore, eventBus)
// Create session manager
sessionManager, err := session.NewManager(eventBus, testStore)
sessionManager, err := session.NewManager(eventBus, testStore, "")
if err != nil {
t.Fatalf("failed to create session manager: %v", err)
}

View File

@@ -18,6 +18,7 @@ type Server struct {
connHandlers map[string]ConnHandlerFunc
subscriptionMgr *SubscriptionHandlers
mu sync.RWMutex
versionOverride string
}
// HandlerFunc is a function that handles an RPC method
@@ -39,6 +40,20 @@ func NewServer() *Server {
return s
}
// NewServerWithVersionOverride creates a new RPC server with a custom version string
func NewServerWithVersionOverride(versionOverride string) *Server {
s := &Server{
handlers: make(map[string]HandlerFunc),
connHandlers: make(map[string]ConnHandlerFunc),
versionOverride: versionOverride,
}
// Register built-in handlers
s.registerBuiltinHandlers()
return s
}
// registerBuiltinHandlers registers the default RPC methods
func (s *Server) registerBuiltinHandlers() {
s.Register("health", s.handleHealthCheck)
@@ -233,8 +248,12 @@ func (s *Server) sendResponse(conn net.Conn, resp *Response) error {
// handleHealthCheck handles the health check RPC method
func (s *Server) handleHealthCheck(ctx context.Context, params json.RawMessage) (interface{}, error) {
version := Version
if s.versionOverride != "" {
version = s.versionOverride
}
return &HealthCheckResponse{
Status: "ok",
Version: Version,
Version: version,
}, nil
}

View File

@@ -22,7 +22,7 @@ func TestContinueSessionInheritance(t *testing.T) {
}
defer func() { _ = sqliteStore.Close() }()
manager, err := NewManager(eventBus, sqliteStore)
manager, err := NewManager(eventBus, sqliteStore, "")
if err != nil {
t.Fatalf("Failed to create manager: %v", err)
}

View File

@@ -26,13 +26,14 @@ type Manager struct {
store store.ConversationStore
approvalReconciler ApprovalReconciler
pendingQueries sync.Map // map[sessionID]query - stores queries waiting for Claude session ID
socketPath string // Daemon socket path for MCP servers
}
// Compile-time check that Manager implements SessionManager
var _ SessionManager = (*Manager)(nil)
// NewManager creates a new session manager with required store
func NewManager(eventBus bus.EventBus, store store.ConversationStore) (*Manager, error) {
func NewManager(eventBus bus.EventBus, store store.ConversationStore, socketPath string) (*Manager, error) {
if store == nil {
return nil, fmt.Errorf("store is required")
}
@@ -47,6 +48,7 @@ func NewManager(eventBus bus.EventBus, store store.ConversationStore) (*Manager,
client: client,
eventBus: eventBus,
store: store,
socketPath: socketPath,
}, nil
}
@@ -63,7 +65,7 @@ func (m *Manager) LaunchSession(ctx context.Context, config claudecode.SessionCo
sessionID := uuid.New().String()
runID := uuid.New().String()
// Add HUMANLAYER_RUN_ID to MCP server environment
// Add HUMANLAYER_RUN_ID and HUMANLAYER_DAEMON_SOCKET to MCP server environment
if config.MCPConfig != nil {
slog.Debug("configuring MCP servers", "count", len(config.MCPConfig.MCPServers))
for name, server := range config.MCPConfig.MCPServers {
@@ -71,12 +73,17 @@ func (m *Manager) LaunchSession(ctx context.Context, config claudecode.SessionCo
server.Env = make(map[string]string)
}
server.Env["HUMANLAYER_RUN_ID"] = runID
// Add daemon socket path so MCP servers connect to the correct daemon
if m.socketPath != "" {
server.Env["HUMANLAYER_DAEMON_SOCKET"] = m.socketPath
}
config.MCPConfig.MCPServers[name] = server
slog.Debug("configured MCP server",
"name", name,
"command", server.Command,
"args", server.Args,
"run_id", runID)
"run_id", runID,
"socket_path", m.socketPath)
}
} else {
slog.Debug("no MCP config provided")
@@ -115,9 +122,29 @@ func (m *Manager) LaunchSession(ctx context.Context, config claudecode.SessionCo
// No longer storing full session in memory
// Log final configuration before launching
var mcpServersDetail string
if config.MCPConfig != nil {
for name, server := range config.MCPConfig.MCPServers {
mcpServersDetail += fmt.Sprintf("[%s: cmd=%s args=%v env=%v] ", name, server.Command, server.Args, server.Env)
}
}
slog.Info("launching Claude session with configuration",
"session_id", sessionID,
"run_id", runID,
"query", config.Query,
"working_dir", config.WorkingDir,
"permission_prompt_tool", config.PermissionPromptTool,
"mcp_servers", len(config.MCPConfig.MCPServers),
"mcp_servers_detail", mcpServersDetail)
// Launch Claude session
claudeSession, err := m.client.Launch(config)
if err != nil {
slog.Error("failed to launch Claude session",
"session_id", sessionID,
"error", err,
"config", fmt.Sprintf("%+v", config))
m.updateSessionStatus(ctx, sessionID, StatusFailed, err.Error())
return nil, fmt.Errorf("failed to launch Claude session: %w", err)
}
@@ -1065,13 +1092,17 @@ func (m *Manager) ContinueSession(ctx context.Context, req ContinueSessionConfig
return nil, fmt.Errorf("failed to store session in database: %w", err)
}
// Add run_id to MCP server environments
// Add run_id and daemon socket to MCP server environments
if config.MCPConfig != nil {
for name, server := range config.MCPConfig.MCPServers {
if server.Env == nil {
server.Env = make(map[string]string)
}
server.Env["HUMANLAYER_RUN_ID"] = runID
// Add daemon socket path so MCP servers connect to the correct daemon
if m.socketPath != "" {
server.Env["HUMANLAYER_DAEMON_SOCKET"] = m.socketPath
}
config.MCPConfig.MCPServers[name] = server
}

View File

@@ -19,7 +19,7 @@ func TestNewManager(t *testing.T) {
mockStore := store.NewMockConversationStore(ctrl)
var eventBus bus.EventBus = nil // no bus for this test
manager, err := NewManager(eventBus, mockStore)
manager, err := NewManager(eventBus, mockStore, "")
if err != nil {
t.Fatalf("Failed to create manager: %v", err)
@@ -34,7 +34,7 @@ func TestNewManager(t *testing.T) {
func TestNewManager_RequiresStore(t *testing.T) {
var eventBus bus.EventBus = nil
_, err := NewManager(eventBus, nil)
_, err := NewManager(eventBus, nil, "")
if err == nil {
t.Fatal("Expected error when store is nil")
@@ -50,7 +50,7 @@ func TestListSessions(t *testing.T) {
defer ctrl.Finish()
mockStore := store.NewMockConversationStore(ctrl)
manager, _ := NewManager(nil, mockStore)
manager, _ := NewManager(nil, mockStore, "")
// Test empty list
mockStore.EXPECT().ListSessions(gomock.Any()).Return([]*store.Session{}, nil)
@@ -83,7 +83,7 @@ func TestGetSessionInfo(t *testing.T) {
defer ctrl.Finish()
mockStore := store.NewMockConversationStore(ctrl)
manager, _ := NewManager(nil, mockStore)
manager, _ := NewManager(nil, mockStore, "")
// Test not found
mockStore.EXPECT().GetSession(gomock.Any(), "not-found").Return(nil, fmt.Errorf("not found"))
@@ -121,7 +121,7 @@ func TestContinueSession_ValidatesParentExists(t *testing.T) {
defer ctrl.Finish()
mockStore := store.NewMockConversationStore(ctrl)
manager, _ := NewManager(nil, mockStore)
manager, _ := NewManager(nil, mockStore, "")
// Test parent not found
mockStore.EXPECT().GetSession(gomock.Any(), "not-found").Return(nil, fmt.Errorf("session not found"))
@@ -144,7 +144,7 @@ func TestContinueSession_ValidatesParentStatus(t *testing.T) {
defer ctrl.Finish()
mockStore := store.NewMockConversationStore(ctrl)
manager, _ := NewManager(nil, mockStore)
manager, _ := NewManager(nil, mockStore, "")
testCases := []struct {
name string
@@ -200,7 +200,7 @@ func TestContinueSession_ValidatesClaudeSessionID(t *testing.T) {
defer ctrl.Finish()
mockStore := store.NewMockConversationStore(ctrl)
manager, _ := NewManager(nil, mockStore)
manager, _ := NewManager(nil, mockStore, "")
// Parent without claude_session_id
parentSession := &store.Session{
@@ -232,7 +232,7 @@ func TestContinueSession_ValidatesWorkingDirectory(t *testing.T) {
defer ctrl.Finish()
mockStore := store.NewMockConversationStore(ctrl)
manager, _ := NewManager(nil, mockStore)
manager, _ := NewManager(nil, mockStore, "")
// Parent without working directory
parentSession := &store.Session{
@@ -264,7 +264,7 @@ func TestContinueSession_CreatesNewSessionWithParentReference(t *testing.T) {
defer ctrl.Finish()
mockStore := store.NewMockConversationStore(ctrl)
manager, _ := NewManager(nil, mockStore)
manager, _ := NewManager(nil, mockStore, "")
// Mock parent session
parentSession := &store.Session{
@@ -340,7 +340,7 @@ func TestContinueSession_HandlesOptionalOverrides(t *testing.T) {
defer ctrl.Finish()
mockStore := store.NewMockConversationStore(ctrl)
manager, _ := NewManager(nil, mockStore)
manager, _ := NewManager(nil, mockStore, "")
// Mock parent session
parentSession := &store.Session{
@@ -439,7 +439,7 @@ func TestInterruptSession(t *testing.T) {
defer ctrl.Finish()
mockStore := store.NewMockConversationStore(ctrl)
manager, _ := NewManager(nil, mockStore)
manager, _ := NewManager(nil, mockStore, "")
// Test interrupting non-existent session
err := manager.InterruptSession(context.Background(), "not-found")
@@ -468,7 +468,7 @@ func TestContinueSession_InterruptsRunningSession(t *testing.T) {
defer ctrl.Finish()
mockStore := store.NewMockConversationStore(ctrl)
manager, _ := NewManager(nil, mockStore)
manager, _ := NewManager(nil, mockStore, "")
t.Run("running session without claude_session_id", func(t *testing.T) {
// Create a running parent session without claude_session_id (orphaned state)

View File

@@ -35,13 +35,18 @@ export const launchCommand = async (query: string, options: LaunchOptions = {})
try {
// Build MCP config (approvals enabled by default unless explicitly disabled)
// For development, use the local built version directly
const scriptPath = new URL(import.meta.url).pathname
const projectRoot = scriptPath.split('/hlyr/')[0] + '/hlyr'
const localHlyrPath = `${projectRoot}/dist/index.js`
const mcpConfig =
options.approvals !== false
? {
mcpServers: {
approvals: {
command: 'npx',
args: ['humanlayer', 'mcp', 'claude_approvals'],
command: 'node',
args: [localHlyrPath, 'mcp', 'claude_approvals'],
},
},
}

View File

@@ -75,7 +75,7 @@ program
.description('HumanLayer, but on your command-line.')
.version(packageJson.version)
const UNPROTECTED_COMMANDS = ['config', 'login', 'thoughts', 'join-waitlist']
const UNPROTECTED_COMMANDS = ['config', 'login', 'thoughts', 'join-waitlist', 'launch', 'mcp']
program.hook('preAction', async (thisCmd, actionCmd) => {
// Get the full command path by traversing up the command hierarchy

View File

@@ -116,8 +116,11 @@ export async function startClaudeApprovalsMCPServer() {
},
)
// Create daemon client
const daemonClient = new DaemonClient()
// Create daemon client with socket path from environment or config
// The daemon sets HUMANLAYER_DAEMON_SOCKET for MCP servers it launches
const socketPath = process.env.HUMANLAYER_DAEMON_SOCKET || config.daemon_socket
logger.info('Creating daemon client', { socketPath })
const daemonClient = new DaemonClient(socketPath)
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {

View File

@@ -1,4 +1,5 @@
use crate::daemon_client::error::{Error, Result};
use std::env;
use std::path::PathBuf;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
@@ -103,6 +104,12 @@ impl Connection {
/// Get the default socket path
fn default_socket_path() -> PathBuf {
// Check environment variable first, matching daemon's behavior
if let Ok(socket_path) = env::var("HUMANLAYER_DAEMON_SOCKET") {
return PathBuf::from(socket_path);
}
// Fall back to default
let home = dirs::home_dir().expect("Could not find home directory");
home.join(DEFAULT_SOCKET_PATH)
}

View File

@@ -120,11 +120,13 @@ export const useSessionLauncher = create<LauncherState>((set, get) => ({
set({ isLaunching: true, error: undefined })
// Build MCP config (approvals enabled by default)
// Use local development version of hlyr instead of npx to avoid version conflicts
const hlyrPath = '/Users/dex/wt/humanlayer/eng-1666/hlyr/dist/index.js'
const mcpConfig = {
mcpServers: {
approvals: {
command: 'npx',
args: ['humanlayer', 'mcp', 'claude_approvals'],
command: 'node',
args: [hlyrPath, 'mcp', 'claude_approvals'],
},
},
}