mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
fix(ssh): stabilize and complete SSH remote execution support across wizard, IPC, and agent detection
- Added full SSH remote support to the wizard, including propagation of SSH config through all phases - Ensured SSH config flows correctly through IPC bridge (renderer → preload → main) - Improved agent detection with optional sshRemoteId and remote `which` resolution - Fixed SSH config transfer during manual agent creation and auto-selection flows - Added extensive debugging and structured logging for SSH execution, agent availability, and wizard message flow - Improved stdin/JSON streaming for large prompts and remote execution - Added input/output JSON stream handling and enhanced logging for SSH transport - Added sourcing of bash profiles to correctly resolve claude-code binary paths on remote hosts
This commit is contained in:
@@ -305,17 +305,117 @@ export function registerAgentsHandlers(deps: AgentsHandlerDependencies): void {
|
||||
})
|
||||
);
|
||||
|
||||
// Get a specific agent by ID
|
||||
ipcMain.handle(
|
||||
'agents:get',
|
||||
withIpcErrorLogging(handlerOpts('get'), async (agentId: string) => {
|
||||
const agentDetector = requireDependency(getAgentDetector, 'Agent detector');
|
||||
logger.debug(`Getting agent: ${agentId}`, LOG_CONTEXT);
|
||||
const agent = await agentDetector.getAgent(agentId);
|
||||
// Strip argBuilder functions before sending over IPC
|
||||
return stripAgentFunctions(agent);
|
||||
})
|
||||
);
|
||||
// Get a specific agent by ID (supports SSH remote detection via optional sshRemoteId)
|
||||
ipcMain.handle(
|
||||
'agents:get',
|
||||
withIpcErrorLogging(handlerOpts('get'), async (agentId: string, sshRemoteId?: string) => {
|
||||
logger.debug(`Getting agent: ${agentId}`, LOG_CONTEXT, { sshRemoteId });
|
||||
|
||||
// If SSH remote ID provided, detect agent on remote host
|
||||
if (sshRemoteId) {
|
||||
const sshConfig = getSshRemoteById(settingsStore, sshRemoteId);
|
||||
if (!sshConfig) {
|
||||
logger.warn(`SSH remote not found or disabled: ${sshRemoteId}`, LOG_CONTEXT);
|
||||
// Return the agent definition with unavailable status
|
||||
const agentDef = AGENT_DEFINITIONS.find((a) => a.id === agentId);
|
||||
if (!agentDef) {
|
||||
throw new Error(`Unknown agent: ${agentId}`);
|
||||
}
|
||||
return stripAgentFunctions({
|
||||
...agentDef,
|
||||
available: false,
|
||||
path: undefined,
|
||||
capabilities: getAgentCapabilities(agentDef.id),
|
||||
error: `SSH remote configuration not found: ${sshRemoteId}`,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`Getting agent ${agentId} on remote host: ${sshConfig.host}`, LOG_CONTEXT);
|
||||
|
||||
// Find the agent definition
|
||||
const agentDef = AGENT_DEFINITIONS.find((a) => a.id === agentId);
|
||||
if (!agentDef) {
|
||||
throw new Error(`Unknown agent: ${agentId}`);
|
||||
}
|
||||
|
||||
// Build SSH command to check for the binary using 'which'
|
||||
const remoteOptions: RemoteCommandOptions = {
|
||||
command: 'which',
|
||||
args: [agentDef.binaryName],
|
||||
};
|
||||
|
||||
try {
|
||||
const sshCommand = await buildSshCommand(sshConfig, remoteOptions);
|
||||
logger.info(`Executing SSH detection command for '${agentDef.binaryName}'`, LOG_CONTEXT, {
|
||||
command: sshCommand.command,
|
||||
args: sshCommand.args,
|
||||
});
|
||||
|
||||
// Execute with timeout
|
||||
const SSH_TIMEOUT_MS = 10000;
|
||||
const resultPromise = execFileNoThrow(sshCommand.command, sshCommand.args);
|
||||
const timeoutPromise = new Promise<{ exitCode: number; stdout: string; stderr: string }>((_, reject) => {
|
||||
setTimeout(() => reject(new Error(`SSH connection timed out after ${SSH_TIMEOUT_MS / 1000}s`)), SSH_TIMEOUT_MS);
|
||||
});
|
||||
|
||||
const result = await Promise.race([resultPromise, timeoutPromise]);
|
||||
|
||||
logger.info(`SSH command result for '${agentDef.binaryName}'`, LOG_CONTEXT, {
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
});
|
||||
|
||||
// Check for SSH connection errors
|
||||
let connectionError: string | undefined;
|
||||
if (result.stderr && (
|
||||
result.stderr.includes('Connection refused') ||
|
||||
result.stderr.includes('Connection timed out') ||
|
||||
result.stderr.includes('No route to host') ||
|
||||
result.stderr.includes('Could not resolve hostname') ||
|
||||
result.stderr.includes('Permission denied')
|
||||
)) {
|
||||
connectionError = result.stderr.trim().split('\n')[0];
|
||||
logger.warn(`SSH connection error for ${sshConfig.host}: ${connectionError}`, LOG_CONTEXT);
|
||||
}
|
||||
|
||||
// Strip ANSI/OSC escape sequences from output
|
||||
const cleanedOutput = stripAnsi(result.stdout);
|
||||
const available = result.exitCode === 0 && cleanedOutput.trim().length > 0;
|
||||
const path = available ? cleanedOutput.trim().split('\n')[0] : undefined;
|
||||
|
||||
if (available) {
|
||||
logger.info(`Agent "${agentDef.name}" found on remote at: ${path}`, LOG_CONTEXT);
|
||||
} else {
|
||||
logger.debug(`Agent "${agentDef.name}" not found on remote`, LOG_CONTEXT);
|
||||
}
|
||||
|
||||
return stripAgentFunctions({
|
||||
...agentDef,
|
||||
available,
|
||||
path,
|
||||
capabilities: getAgentCapabilities(agentDef.id),
|
||||
error: connectionError,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.warn(`Failed to check agent "${agentDef.name}" on remote: ${errorMessage}`, LOG_CONTEXT);
|
||||
return stripAgentFunctions({
|
||||
...agentDef,
|
||||
available: false,
|
||||
capabilities: getAgentCapabilities(agentDef.id),
|
||||
error: `Failed to connect: ${errorMessage}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Local detection
|
||||
const agentDetector = requireDependency(getAgentDetector, 'Agent detector');
|
||||
const agent = await agentDetector.getAgent(agentId);
|
||||
// Strip argBuilder functions before sending over IPC
|
||||
return stripAgentFunctions(agent);
|
||||
})
|
||||
);
|
||||
|
||||
// Get capabilities for a specific agent
|
||||
ipcMain.handle(
|
||||
|
||||
@@ -20,6 +20,7 @@ import { buildSshCommand } from '../../utils/ssh-command-builder';
|
||||
import type { SshRemoteConfig } from '../../../shared/types';
|
||||
import { powerManager } from '../../power-manager';
|
||||
import { MaestroSettings } from './persistence';
|
||||
import { getAgentCapabilities } from '../../agent-capabilities';
|
||||
|
||||
const LOG_CONTEXT = '[ProcessManager]';
|
||||
|
||||
@@ -108,42 +109,46 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
||||
const processManager = requireProcessManager(getProcessManager);
|
||||
const agentDetector = requireDependency(getAgentDetector, 'Agent detector');
|
||||
|
||||
// Get agent definition to access config options and argument builders
|
||||
const agent = await agentDetector.getAgent(config.toolType);
|
||||
// Use INFO level on Windows for better visibility in logs
|
||||
const isWindows = process.platform === 'win32';
|
||||
const logFn = isWindows ? logger.info.bind(logger) : logger.debug.bind(logger);
|
||||
logFn(`Spawn config received`, LOG_CONTEXT, {
|
||||
platform: process.platform,
|
||||
configToolType: config.toolType,
|
||||
configCommand: config.command,
|
||||
agentId: agent?.id,
|
||||
agentCommand: agent?.command,
|
||||
agentPath: agent?.path,
|
||||
agentPathExtension: agent?.path ? require('path').extname(agent.path) : 'none',
|
||||
hasAgentSessionId: !!config.agentSessionId,
|
||||
hasPrompt: !!config.prompt,
|
||||
promptLength: config.prompt?.length,
|
||||
// On Windows, show prompt preview to help debug truncation issues
|
||||
promptPreview:
|
||||
config.prompt && isWindows
|
||||
? {
|
||||
first50: config.prompt.substring(0, 50),
|
||||
last50: config.prompt.substring(Math.max(0, config.prompt.length - 50)),
|
||||
containsHash: config.prompt.includes('#'),
|
||||
containsNewline: config.prompt.includes('\n'),
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
let finalArgs = buildAgentArgs(agent, {
|
||||
baseArgs: config.args,
|
||||
prompt: config.prompt,
|
||||
cwd: config.cwd,
|
||||
readOnlyMode: config.readOnlyMode,
|
||||
modelId: config.modelId,
|
||||
yoloMode: config.yoloMode,
|
||||
agentSessionId: config.agentSessionId,
|
||||
});
|
||||
// Get agent definition to access config options and argument builders
|
||||
const agent = await agentDetector.getAgent(config.toolType);
|
||||
// Use INFO level on Windows for better visibility in logs
|
||||
const isWindows = process.platform === 'win32';
|
||||
const logFn = isWindows ? logger.info.bind(logger) : logger.debug.bind(logger);
|
||||
logFn(`Spawn config received`, LOG_CONTEXT, {
|
||||
platform: process.platform,
|
||||
configToolType: config.toolType,
|
||||
configCommand: config.command,
|
||||
agentId: agent?.id,
|
||||
agentCommand: agent?.command,
|
||||
agentPath: agent?.path,
|
||||
agentPathExtension: agent?.path ? require('path').extname(agent.path) : 'none',
|
||||
hasAgentSessionId: !!config.agentSessionId,
|
||||
hasPrompt: !!config.prompt,
|
||||
promptLength: config.prompt?.length,
|
||||
// On Windows, show prompt preview to help debug truncation issues
|
||||
promptPreview: config.prompt && isWindows ? {
|
||||
first50: config.prompt.substring(0, 50),
|
||||
last50: config.prompt.substring(Math.max(0, config.prompt.length - 50)),
|
||||
containsHash: config.prompt.includes('#'),
|
||||
containsNewline: config.prompt.includes('\n'),
|
||||
} : undefined,
|
||||
// SSH remote config logging
|
||||
hasSessionSshRemoteConfig: !!config.sessionSshRemoteConfig,
|
||||
sessionSshRemoteConfig: config.sessionSshRemoteConfig ? {
|
||||
enabled: config.sessionSshRemoteConfig.enabled,
|
||||
remoteId: config.sessionSshRemoteConfig.remoteId,
|
||||
hasWorkingDirOverride: !!config.sessionSshRemoteConfig.workingDirOverride,
|
||||
} : null,
|
||||
});
|
||||
let finalArgs = buildAgentArgs(agent, {
|
||||
baseArgs: config.args,
|
||||
prompt: config.prompt,
|
||||
cwd: config.cwd,
|
||||
readOnlyMode: config.readOnlyMode,
|
||||
modelId: config.modelId,
|
||||
yoloMode: config.yoloMode,
|
||||
agentSessionId: config.agentSessionId,
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// Apply agent config options and session overrides
|
||||
@@ -252,21 +257,31 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SSH Remote Execution: Detect and wrap command for remote execution
|
||||
// Terminal sessions are always local (they need PTY for shell interaction)
|
||||
// ========================================================================
|
||||
let sshRemoteUsed: SshRemoteConfig | null = null;
|
||||
// ========================================================================
|
||||
// SSH Remote Execution: Detect and wrap command for remote execution
|
||||
// Terminal sessions are always local (they need PTY for shell interaction)
|
||||
// ========================================================================
|
||||
let sshRemoteUsed: SshRemoteConfig | null = null;
|
||||
let shouldSendPromptViaStdin = false;
|
||||
|
||||
// Only consider SSH remote for non-terminal AI agent sessions
|
||||
// SSH is session-level ONLY - no agent-level or global defaults
|
||||
if (config.toolType !== 'terminal' && config.sessionSshRemoteConfig) {
|
||||
// Session-level SSH config provided - resolve and use it
|
||||
logger.debug(`Using session-level SSH config`, LOG_CONTEXT, {
|
||||
sessionId: config.sessionId,
|
||||
enabled: config.sessionSshRemoteConfig.enabled,
|
||||
remoteId: config.sessionSshRemoteConfig.remoteId,
|
||||
});
|
||||
// Only consider SSH remote for non-terminal AI agent sessions
|
||||
// SSH is session-level ONLY - no agent-level or global defaults
|
||||
// Log SSH evaluation on Windows for debugging
|
||||
if (isWindows) {
|
||||
logger.info(`Evaluating SSH remote config`, LOG_CONTEXT, {
|
||||
toolType: config.toolType,
|
||||
isTerminal: config.toolType === 'terminal',
|
||||
hasSessionSshRemoteConfig: !!config.sessionSshRemoteConfig,
|
||||
willUseSsh: config.toolType !== 'terminal' && !!config.sessionSshRemoteConfig,
|
||||
});
|
||||
}
|
||||
if (config.toolType !== 'terminal' && config.sessionSshRemoteConfig) {
|
||||
// Session-level SSH config provided - resolve and use it
|
||||
logger.info(`Using session-level SSH config`, LOG_CONTEXT, {
|
||||
sessionId: config.sessionId,
|
||||
enabled: config.sessionSshRemoteConfig.enabled,
|
||||
remoteId: config.sessionSshRemoteConfig.remoteId,
|
||||
});
|
||||
|
||||
// Resolve effective SSH remote configuration
|
||||
const sshStoreAdapter = createSshRemoteStoreAdapter(settingsStore);
|
||||
@@ -278,19 +293,31 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
||||
// SSH remote is configured - wrap the command for remote execution
|
||||
sshRemoteUsed = sshResult.config;
|
||||
|
||||
// For SSH execution, we need to include the prompt in the args here
|
||||
// because ProcessManager.spawn() won't add it (we pass prompt: undefined for SSH)
|
||||
// Use promptArgs if available (e.g., OpenCode -p), otherwise use positional arg
|
||||
let sshArgs = finalArgs;
|
||||
if (config.prompt) {
|
||||
if (agent?.promptArgs) {
|
||||
sshArgs = [...finalArgs, ...agent.promptArgs(config.prompt)];
|
||||
} else if (agent?.noPromptSeparator) {
|
||||
sshArgs = [...finalArgs, config.prompt];
|
||||
} else {
|
||||
sshArgs = [...finalArgs, '--', config.prompt];
|
||||
}
|
||||
}
|
||||
// For SSH execution with stream-json capable agents (like Claude Code),
|
||||
// we send the prompt via stdin instead of as a CLI arg to avoid command
|
||||
// line length limits and escaping issues. For other agents, we include
|
||||
// the prompt in the args as before.
|
||||
const capabilities = getAgentCapabilities(config.toolType);
|
||||
let sshArgs = finalArgs;
|
||||
|
||||
if (config.prompt) {
|
||||
// If agent supports stream-json input, send prompt via stdin
|
||||
// ProcessManager will detect this and send the prompt as a JSON message
|
||||
if (capabilities.supportsStreamJsonInput) {
|
||||
shouldSendPromptViaStdin = true;
|
||||
// Add --input-format stream-json flag so Claude knows to read from stdin
|
||||
sshArgs = [...finalArgs, '--input-format', 'stream-json'];
|
||||
} else {
|
||||
// For agents that don't support stream-json, add prompt to args
|
||||
if (agent?.promptArgs) {
|
||||
sshArgs = [...finalArgs, ...agent.promptArgs(config.prompt)];
|
||||
} else if (agent?.noPromptSeparator) {
|
||||
sshArgs = [...finalArgs, config.prompt];
|
||||
} else {
|
||||
sshArgs = [...finalArgs, '--', config.prompt];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the SSH command that wraps the agent execution
|
||||
//
|
||||
@@ -312,51 +339,53 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
||||
commandToSpawn = sshCommand.command;
|
||||
argsToSpawn = sshCommand.args;
|
||||
|
||||
logger.info(`SSH remote execution configured`, LOG_CONTEXT, {
|
||||
sessionId: config.sessionId,
|
||||
toolType: config.toolType,
|
||||
remoteName: sshResult.config.name,
|
||||
remoteHost: sshResult.config.host,
|
||||
source: sshResult.source,
|
||||
localCommand: config.command,
|
||||
remoteCommand: remoteCommand,
|
||||
customPath: config.sessionCustomPath || null,
|
||||
hasCustomEnvVars:
|
||||
!!effectiveCustomEnvVars && Object.keys(effectiveCustomEnvVars).length > 0,
|
||||
sshCommand: `${sshCommand.command} ${sshCommand.args.join(' ')}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
logger.info(`SSH remote execution configured`, LOG_CONTEXT, {
|
||||
sessionId: config.sessionId,
|
||||
toolType: config.toolType,
|
||||
remoteName: sshResult.config.name,
|
||||
remoteHost: sshResult.config.host,
|
||||
source: sshResult.source,
|
||||
localCommand: config.command,
|
||||
remoteCommand: remoteCommand,
|
||||
customPath: config.sessionCustomPath || null,
|
||||
hasCustomEnvVars: !!effectiveCustomEnvVars && Object.keys(effectiveCustomEnvVars).length > 0,
|
||||
sshCommand: `${sshCommand.command} ${sshCommand.args.join(' ')}`,
|
||||
promptViaStdin: shouldSendPromptViaStdin,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = processManager.spawn({
|
||||
...config,
|
||||
command: commandToSpawn,
|
||||
args: argsToSpawn,
|
||||
// When using SSH, use user's home directory as local cwd
|
||||
// The remote working directory is embedded in the SSH command itself
|
||||
// This fixes ENOENT errors when session.cwd is a remote-only path
|
||||
cwd: sshRemoteUsed ? os.homedir() : config.cwd,
|
||||
// When using SSH, disable PTY (SSH provides its own terminal handling)
|
||||
// and env vars are passed via the remote command string
|
||||
requiresPty: sshRemoteUsed ? false : agent?.requiresPty,
|
||||
// When using SSH, the prompt was already added to sshArgs above before
|
||||
// building the SSH command, so don't let ProcessManager add it again
|
||||
prompt: sshRemoteUsed ? undefined : config.prompt,
|
||||
shell: shellToUse,
|
||||
shellArgs: shellArgsStr, // Shell-specific CLI args (for terminal sessions)
|
||||
shellEnvVars: shellEnvVars, // Shell-specific env vars (for terminal sessions)
|
||||
contextWindow, // Pass configured context window to process manager
|
||||
// When using SSH, env vars are passed in the remote command string, not locally
|
||||
customEnvVars: sshRemoteUsed ? undefined : effectiveCustomEnvVars,
|
||||
imageArgs: agent?.imageArgs, // Function to build image CLI args (for Codex, OpenCode)
|
||||
promptArgs: agent?.promptArgs, // Function to build prompt args (e.g., ['-p', prompt] for OpenCode)
|
||||
noPromptSeparator: agent?.noPromptSeparator, // Some agents don't support '--' before prompt
|
||||
// Stats tracking: use cwd as projectPath if not explicitly provided
|
||||
projectPath: config.cwd,
|
||||
// SSH remote context (for SSH-specific error messages)
|
||||
sshRemoteId: sshRemoteUsed?.id,
|
||||
sshRemoteHost: sshRemoteUsed?.host,
|
||||
});
|
||||
const result = processManager.spawn({
|
||||
...config,
|
||||
command: commandToSpawn,
|
||||
args: argsToSpawn,
|
||||
// When using SSH, use user's home directory as local cwd
|
||||
// The remote working directory is embedded in the SSH command itself
|
||||
// This fixes ENOENT errors when session.cwd is a remote-only path
|
||||
cwd: sshRemoteUsed ? os.homedir() : config.cwd,
|
||||
// When using SSH, disable PTY (SSH provides its own terminal handling)
|
||||
// and env vars are passed via the remote command string
|
||||
requiresPty: sshRemoteUsed ? false : agent?.requiresPty,
|
||||
// When using SSH with stream-json capable agents, pass the prompt so
|
||||
// ProcessManager can send it via stdin. For other SSH cases, prompt was
|
||||
// already added to sshArgs, so pass undefined to prevent double-adding.
|
||||
prompt: sshRemoteUsed && shouldSendPromptViaStdin ? config.prompt : sshRemoteUsed ? undefined : config.prompt,
|
||||
shell: shellToUse,
|
||||
shellArgs: shellArgsStr, // Shell-specific CLI args (for terminal sessions)
|
||||
shellEnvVars: shellEnvVars, // Shell-specific env vars (for terminal sessions)
|
||||
contextWindow, // Pass configured context window to process manager
|
||||
// When using SSH, env vars are passed in the remote command string, not locally
|
||||
customEnvVars: sshRemoteUsed ? undefined : effectiveCustomEnvVars,
|
||||
imageArgs: agent?.imageArgs, // Function to build image CLI args (for Codex, OpenCode)
|
||||
promptArgs: agent?.promptArgs, // Function to build prompt args (e.g., ['-p', prompt] for OpenCode)
|
||||
noPromptSeparator: agent?.noPromptSeparator, // Some agents don't support '--' before prompt
|
||||
// Stats tracking: use cwd as projectPath if not explicitly provided
|
||||
projectPath: config.cwd,
|
||||
// SSH remote context (for SSH-specific error messages)
|
||||
sshRemoteConfig: sshRemoteUsed,
|
||||
sshRemoteId: sshRemoteUsed?.id,
|
||||
sshRemoteHost: sshRemoteUsed?.host,
|
||||
});
|
||||
|
||||
logger.info(`Process spawned successfully`, LOG_CONTEXT, {
|
||||
sessionId: config.sessionId,
|
||||
|
||||
@@ -237,37 +237,41 @@ export async function buildSshCommand(
|
||||
env: Object.keys(mergedEnv).length > 0 ? mergedEnv : undefined,
|
||||
});
|
||||
|
||||
// Wrap the command to execute via the user's login shell.
|
||||
// $SHELL -ilc ensures the user's full PATH (including homebrew, nvm, etc.) is available.
|
||||
// -i forces interactive mode (critical for .bashrc to not bail out)
|
||||
// -l loads login profile for PATH
|
||||
// -c executes the command
|
||||
// Using $SHELL respects the user's configured shell (bash, zsh, etc.)
|
||||
//
|
||||
// WHY -i IS CRITICAL:
|
||||
// On Ubuntu (and many Linux distros), .bashrc has a guard at the top:
|
||||
// case $- in *i*) ;; *) return;; esac
|
||||
// This checks if the shell is interactive before running. Without -i,
|
||||
// .bashrc exits early and user PATH additions (like ~/.local/bin) never load.
|
||||
// The -i flag sets 'i' in $-, allowing .bashrc to run fully.
|
||||
//
|
||||
// CRITICAL: When Node.js spawn() passes this to SSH without shell:true, SSH runs
|
||||
// the command through the remote's default shell. The key is escaping:
|
||||
// 1. Double quotes around the command are NOT escaped - they delimit the -c argument
|
||||
// 2. $ signs inside the command MUST be escaped as \$ so they defer to the login shell
|
||||
// (shellEscapeForDoubleQuotes handles this)
|
||||
// 3. Single quotes inside the command pass through unchanged
|
||||
//
|
||||
// Example transformation for spawn():
|
||||
// Input: cd '/path' && MYVAR='value' claude --print
|
||||
// After escaping: cd '/path' && MYVAR='value' claude --print (no $ to escape here)
|
||||
// Wrapped: $SHELL -ilc "cd '/path' && MYVAR='value' claude --print"
|
||||
// SSH receives this as one argument, passes to remote shell
|
||||
// Remote shell expands $SHELL, executes: /bin/zsh -ilc "cd '/path' ..."
|
||||
// The login shell runs with full PATH from ~/.zprofile AND ~/.bashrc
|
||||
const escapedCommand = shellEscapeForDoubleQuotes(remoteCommand);
|
||||
const wrappedCommand = `$SHELL -ilc "${escapedCommand}"`;
|
||||
args.push(wrappedCommand);
|
||||
// Wrap the command to execute via the user's login shell.
|
||||
// We use bash -lc to ensure the user's full PATH (including homebrew, nvm, etc.) is available.
|
||||
// -l loads login profile for PATH (sources /etc/profile, ~/.bash_profile, etc.)
|
||||
// -c executes the command
|
||||
//
|
||||
// IMPORTANT: We don't use -i (interactive) flag because:
|
||||
// 1. It can cause issues with shells that check if stdin is a TTY
|
||||
// 2. When using -tt flag, the shell already thinks it's interactive enough
|
||||
// 3. On Ubuntu, we work around the "case $- in *i*)" guard by sourcing ~/.bashrc explicitly
|
||||
//
|
||||
// For bash specifically, we source ~/.bashrc to ensure user PATH additions are loaded.
|
||||
// This handles the common Ubuntu pattern where .bashrc has:
|
||||
// case $- in *i*) ;; *) return;; esac
|
||||
// By explicitly sourcing it, we bypass this guard.
|
||||
//
|
||||
// CRITICAL: When Node.js spawn() passes this to SSH without shell:true, SSH runs
|
||||
// the command through the remote's default shell. The key is escaping:
|
||||
// 1. Double quotes around the command are NOT escaped - they delimit the -c argument
|
||||
// 2. $ signs inside the command MUST be escaped as \$ so they defer to the login shell
|
||||
// (shellEscapeForDoubleQuotes handles this)
|
||||
// 3. Single quotes inside the command pass through unchanged
|
||||
//
|
||||
// Example transformation for spawn():
|
||||
// Input: cd '/path' && MYVAR='value' claude --print
|
||||
// After escaping: cd '/path' && MYVAR='value' claude --print (no $ to escape here)
|
||||
// Wrapped: bash -lc "source ~/.bashrc 2>/dev/null; cd '/path' && MYVAR='value' claude --print"
|
||||
// SSH receives this as one argument, passes to remote shell
|
||||
// The login shell runs with full PATH from /etc/profile, ~/.bash_profile, AND ~/.bashrc
|
||||
const escapedCommand = shellEscapeForDoubleQuotes(remoteCommand);
|
||||
// Source login/profile files first so PATH modifications made in
|
||||
// ~/.bash_profile or ~/.profile are available for non-interactive
|
||||
// remote commands, then source ~/.bashrc to cover interactive
|
||||
// additions that might also be present.
|
||||
const wrappedCommand = `bash -lc "source ~/.bash_profile 2>/dev/null || source ~/.profile 2>/dev/null; source ~/.bashrc 2>/dev/null; ${escapedCommand}"`;
|
||||
args.push(wrappedCommand);
|
||||
|
||||
// Debug logging to trace the exact command being built
|
||||
logger.info('Built SSH command', '[ssh-command-builder]', {
|
||||
|
||||
@@ -578,23 +578,34 @@ export function NewInstanceModal({
|
||||
}
|
||||
}, [isOpen, sourceSession]);
|
||||
|
||||
// Load SSH remote configurations independently of agent detection
|
||||
// This ensures SSH remotes are available even if agent detection fails
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const loadSshConfigs = async () => {
|
||||
try {
|
||||
const sshConfigsResult = await window.maestro.sshRemote.getConfigs();
|
||||
if (sshConfigsResult.success && sshConfigsResult.configs) {
|
||||
setSshRemotes(sshConfigsResult.configs);
|
||||
}
|
||||
} catch (sshError) {
|
||||
console.error('Failed to load SSH remote configs:', sshError);
|
||||
}
|
||||
};
|
||||
loadSshConfigs();
|
||||
}
|
||||
}, [isOpen]);
|
||||
// Load SSH remote configurations independently of agent detection
|
||||
// This ensures SSH remotes are available even if agent detection fails
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const loadSshConfigs = async () => {
|
||||
try {
|
||||
const sshConfigsResult = await window.maestro.sshRemote.getConfigs();
|
||||
if (sshConfigsResult.success && sshConfigsResult.configs) {
|
||||
setSshRemotes(sshConfigsResult.configs);
|
||||
}
|
||||
} catch (sshError) {
|
||||
console.error('Failed to load SSH remote configs:', sshError);
|
||||
}
|
||||
};
|
||||
loadSshConfigs();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Transfer pending SSH config to selected agent automatically
|
||||
// This ensures SSH config is preserved when agent is auto-selected or manually clicked
|
||||
useEffect(() => {
|
||||
if (selectedAgent && agentSshRemoteConfigs['_pending_'] && !agentSshRemoteConfigs[selectedAgent]) {
|
||||
setAgentSshRemoteConfigs(prev => ({
|
||||
...prev,
|
||||
[selectedAgent]: prev['_pending_'],
|
||||
}));
|
||||
}
|
||||
}, [selectedAgent, agentSshRemoteConfigs]);
|
||||
|
||||
// Track the current SSH remote ID for re-detection
|
||||
// Uses _pending_ key when no agent is selected, which is the shared SSH config
|
||||
|
||||
@@ -680,12 +680,13 @@ export function ConversationScreen({
|
||||
// Fetch existing docs if continuing from previous session
|
||||
const existingDocs = await fetchExistingDocs();
|
||||
|
||||
await conversationManager.startConversation({
|
||||
agentType: state.selectedAgent,
|
||||
directoryPath: state.directoryPath,
|
||||
projectName: state.agentName || 'My Project',
|
||||
existingDocs: existingDocs.length > 0 ? existingDocs : undefined,
|
||||
});
|
||||
await conversationManager.startConversation({
|
||||
agentType: state.selectedAgent,
|
||||
directoryPath: state.directoryPath,
|
||||
projectName: state.agentName || 'My Project',
|
||||
existingDocs: existingDocs.length > 0 ? existingDocs : undefined,
|
||||
sessionSshRemoteConfig: state.sessionSshRemoteConfig,
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
setConversationStarted(true);
|
||||
@@ -1061,13 +1062,14 @@ export function ConversationScreen({
|
||||
}
|
||||
}
|
||||
|
||||
await conversationManager.startConversation({
|
||||
agentType: state.selectedAgent,
|
||||
directoryPath: state.directoryPath,
|
||||
projectName: state.agentName || 'My Project',
|
||||
existingDocs: existingDocs.length > 0 ? existingDocs : undefined,
|
||||
});
|
||||
}
|
||||
await conversationManager.startConversation({
|
||||
agentType: state.selectedAgent,
|
||||
directoryPath: state.directoryPath,
|
||||
projectName: state.agentName || 'My Project',
|
||||
existingDocs: existingDocs.length > 0 ? existingDocs : undefined,
|
||||
sessionSshRemoteConfig: state.sessionSshRemoteConfig,
|
||||
});
|
||||
}
|
||||
|
||||
// Send message and wait for response
|
||||
const result = await conversationManager.sendMessage(
|
||||
|
||||
@@ -29,14 +29,20 @@ import { wizardDebugLogger } from './phaseGenerator';
|
||||
* Configuration for starting a conversation
|
||||
*/
|
||||
export interface ConversationConfig {
|
||||
/** The agent type to use for the conversation */
|
||||
agentType: ToolType;
|
||||
/** The working directory for the agent */
|
||||
directoryPath: string;
|
||||
/** Project name (used in system prompt) */
|
||||
projectName: string;
|
||||
/** Existing Auto Run documents (when continuing from previous session) */
|
||||
existingDocs?: ExistingDocument[];
|
||||
/** The agent type to use for the conversation */
|
||||
agentType: ToolType;
|
||||
/** The working directory for the agent */
|
||||
directoryPath: string;
|
||||
/** Project name (used in system prompt) */
|
||||
projectName: string;
|
||||
/** Existing Auto Run documents (when continuing from previous session) */
|
||||
existingDocs?: ExistingDocument[];
|
||||
/** SSH remote configuration for remote agent execution */
|
||||
sessionSshRemoteConfig?: {
|
||||
enabled: boolean;
|
||||
remoteId: string | null;
|
||||
workingDirOverride?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,34 +90,40 @@ export interface ConversationCallbacks {
|
||||
* State of an active conversation session
|
||||
*/
|
||||
interface ConversationSession {
|
||||
/** Unique session ID for this wizard conversation */
|
||||
sessionId: string;
|
||||
/** The agent type */
|
||||
agentType: ToolType;
|
||||
/** Working directory */
|
||||
directoryPath: string;
|
||||
/** Project name */
|
||||
projectName: string;
|
||||
/** Whether the agent process is active */
|
||||
isActive: boolean;
|
||||
/** System prompt used for this session */
|
||||
systemPrompt: string;
|
||||
/** Accumulated output buffer for parsing */
|
||||
outputBuffer: string;
|
||||
/** Resolve function for pending message */
|
||||
pendingResolve?: (result: SendMessageResult) => void;
|
||||
/** Callbacks for the conversation */
|
||||
callbacks?: ConversationCallbacks;
|
||||
/** Cleanup function for data listener */
|
||||
dataListenerCleanup?: () => void;
|
||||
/** Cleanup function for exit listener */
|
||||
exitListenerCleanup?: () => void;
|
||||
/** Cleanup function for thinking chunk listener */
|
||||
thinkingListenerCleanup?: () => void;
|
||||
/** Cleanup function for tool execution listener */
|
||||
toolExecutionListenerCleanup?: () => void;
|
||||
/** Timeout ID for response timeout (for cleanup) */
|
||||
responseTimeoutId?: NodeJS.Timeout;
|
||||
/** Unique session ID for this wizard conversation */
|
||||
sessionId: string;
|
||||
/** The agent type */
|
||||
agentType: ToolType;
|
||||
/** Working directory */
|
||||
directoryPath: string;
|
||||
/** Project name */
|
||||
projectName: string;
|
||||
/** Whether the agent process is active */
|
||||
isActive: boolean;
|
||||
/** System prompt used for this session */
|
||||
systemPrompt: string;
|
||||
/** Accumulated output buffer for parsing */
|
||||
outputBuffer: string;
|
||||
/** Resolve function for pending message */
|
||||
pendingResolve?: (result: SendMessageResult) => void;
|
||||
/** Callbacks for the conversation */
|
||||
callbacks?: ConversationCallbacks;
|
||||
/** Cleanup function for data listener */
|
||||
dataListenerCleanup?: () => void;
|
||||
/** Cleanup function for exit listener */
|
||||
exitListenerCleanup?: () => void;
|
||||
/** Cleanup function for thinking chunk listener */
|
||||
thinkingListenerCleanup?: () => void;
|
||||
/** Cleanup function for tool execution listener */
|
||||
toolExecutionListenerCleanup?: () => void;
|
||||
/** Timeout ID for response timeout (for cleanup) */
|
||||
responseTimeoutId?: NodeJS.Timeout;
|
||||
/** SSH remote configuration for remote execution */
|
||||
sessionSshRemoteConfig?: {
|
||||
enabled: boolean;
|
||||
remoteId: string | null;
|
||||
workingDirOverride?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,25 +166,28 @@ class ConversationManager {
|
||||
existingDocs: config.existingDocs,
|
||||
});
|
||||
|
||||
this.session = {
|
||||
sessionId,
|
||||
agentType: config.agentType,
|
||||
directoryPath: config.directoryPath,
|
||||
projectName: config.projectName,
|
||||
isActive: true,
|
||||
systemPrompt,
|
||||
outputBuffer: '',
|
||||
};
|
||||
this.session = {
|
||||
sessionId,
|
||||
agentType: config.agentType,
|
||||
directoryPath: config.directoryPath,
|
||||
projectName: config.projectName,
|
||||
isActive: true,
|
||||
systemPrompt,
|
||||
outputBuffer: '',
|
||||
sessionSshRemoteConfig: config.sessionSshRemoteConfig,
|
||||
};
|
||||
|
||||
// Log conversation start
|
||||
wizardDebugLogger.log('info', 'Conversation started', {
|
||||
sessionId,
|
||||
agentType: config.agentType,
|
||||
directoryPath: config.directoryPath,
|
||||
projectName: config.projectName,
|
||||
hasExistingDocs: !!config.existingDocs,
|
||||
existingDocsCount: config.existingDocs?.length || 0,
|
||||
});
|
||||
// Log conversation start
|
||||
wizardDebugLogger.log('info', 'Conversation started', {
|
||||
sessionId,
|
||||
agentType: config.agentType,
|
||||
directoryPath: config.directoryPath,
|
||||
projectName: config.projectName,
|
||||
hasExistingDocs: !!config.existingDocs,
|
||||
existingDocsCount: config.existingDocs?.length || 0,
|
||||
hasRemoteSsh: !!config.sessionSshRemoteConfig?.enabled,
|
||||
remoteId: config.sessionSshRemoteConfig?.remoteId || null,
|
||||
});
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
@@ -217,20 +232,61 @@ class ConversationManager {
|
||||
// Notify sending
|
||||
callbacks?.onSending?.();
|
||||
|
||||
try {
|
||||
// Get the agent configuration
|
||||
const agent = await window.maestro.agents.get(this.session.agentType);
|
||||
if (!agent || !agent.available) {
|
||||
const error = `Agent ${this.session.agentType} is not available`;
|
||||
wizardDebugLogger.log('error', 'Agent not available', {
|
||||
agentType: this.session.agentType,
|
||||
agent: agent ? { available: agent.available } : null,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
try {
|
||||
// Get the agent configuration
|
||||
// Pass SSH remote ID if SSH is enabled for this session
|
||||
const sshRemoteId = this.session.sessionSshRemoteConfig?.enabled
|
||||
? this.session.sessionSshRemoteConfig.remoteId
|
||||
: undefined;
|
||||
|
||||
wizardDebugLogger.log('info', 'Fetching agent configuration', {
|
||||
agentType: this.session.agentType,
|
||||
sessionId: this.session.sessionId,
|
||||
hasRemoteSsh: !!this.session.sessionSshRemoteConfig?.enabled,
|
||||
remoteId: this.session.sessionSshRemoteConfig?.remoteId || null,
|
||||
passingSshRemoteId: sshRemoteId || null,
|
||||
});
|
||||
|
||||
const agent = await window.maestro.agents.get(this.session.agentType, sshRemoteId || undefined);
|
||||
|
||||
// Log to main process (writes to maestro-debug.log on Windows)
|
||||
console.log('[Wizard] Agent fetch result:', {
|
||||
agentType: this.session.agentType,
|
||||
agentExists: !!agent,
|
||||
agentAvailable: agent?.available,
|
||||
agentPath: agent?.path,
|
||||
agentCommand: agent?.command,
|
||||
hasRemoteSsh: !!this.session.sessionSshRemoteConfig?.enabled,
|
||||
});
|
||||
|
||||
if (!agent || !agent.available) {
|
||||
const error = `Agent ${this.session.agentType} is not available`;
|
||||
|
||||
// Log detailed info about why agent is unavailable
|
||||
console.error('[Wizard] Agent not available - Details:', {
|
||||
agentType: this.session.agentType,
|
||||
agentExists: !!agent,
|
||||
agentAvailable: agent?.available,
|
||||
agentPath: agent?.path,
|
||||
agentError: (agent as any)?.error,
|
||||
sessionSshConfig: this.session.sessionSshRemoteConfig,
|
||||
});
|
||||
|
||||
wizardDebugLogger.log('error', 'Agent not available', {
|
||||
agentType: this.session.agentType,
|
||||
agent: agent ? {
|
||||
available: agent.available,
|
||||
path: agent.path,
|
||||
error: (agent as any).error
|
||||
} : null,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
console.log('[Wizard] Agent is available, building prompt...');
|
||||
|
||||
// Build the full prompt with conversation context
|
||||
const fullPrompt = this.buildPromptWithContext(userMessage, conversationHistory);
|
||||
@@ -480,44 +536,48 @@ class ConversationManager {
|
||||
// This is critical for packaged Electron apps where PATH may not include agent locations
|
||||
const commandToUse = agent.path || agent.command;
|
||||
|
||||
wizardDebugLogger.log('spawn', 'Calling process.spawn', {
|
||||
sessionId: this.session!.sessionId,
|
||||
command: commandToUse,
|
||||
agentPath: agent.path,
|
||||
agentCommand: agent.command,
|
||||
args: argsForSpawn,
|
||||
cwd: this.session!.directoryPath,
|
||||
});
|
||||
wizardDebugLogger.log('spawn', 'Calling process.spawn', {
|
||||
sessionId: this.session!.sessionId,
|
||||
command: commandToUse,
|
||||
agentPath: agent.path,
|
||||
agentCommand: agent.command,
|
||||
args: argsForSpawn,
|
||||
cwd: this.session!.directoryPath,
|
||||
hasRemoteSsh: !!this.session!.sessionSshRemoteConfig?.enabled,
|
||||
remoteId: this.session!.sessionSshRemoteConfig?.remoteId || null,
|
||||
});
|
||||
|
||||
window.maestro.process
|
||||
.spawn({
|
||||
sessionId: this.session!.sessionId,
|
||||
toolType: this.session!.agentType,
|
||||
cwd: this.session!.directoryPath,
|
||||
command: commandToUse,
|
||||
args: argsForSpawn,
|
||||
prompt: prompt,
|
||||
sessionSshRemoteConfig: this.session!.sessionSshRemoteConfig,
|
||||
})
|
||||
.then(() => {
|
||||
wizardDebugLogger.log('spawn', 'Agent process spawned successfully', {
|
||||
sessionId: this.session?.sessionId,
|
||||
});
|
||||
// Notify that we're receiving
|
||||
this.session?.callbacks?.onReceiving?.();
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
wizardDebugLogger.log('error', 'Failed to spawn agent process', {
|
||||
sessionId: this.session?.sessionId,
|
||||
error: error.message,
|
||||
});
|
||||
this.cleanupListeners();
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to spawn agent: ${error.message}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.maestro.process
|
||||
.spawn({
|
||||
sessionId: this.session!.sessionId,
|
||||
toolType: this.session!.agentType,
|
||||
cwd: this.session!.directoryPath,
|
||||
command: commandToUse,
|
||||
args: argsForSpawn,
|
||||
prompt: prompt,
|
||||
})
|
||||
.then(() => {
|
||||
wizardDebugLogger.log('spawn', 'Agent process spawned successfully', {
|
||||
sessionId: this.session?.sessionId,
|
||||
});
|
||||
// Notify that we're receiving
|
||||
this.session?.callbacks?.onReceiving?.();
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
wizardDebugLogger.log('error', 'Failed to spawn agent process', {
|
||||
sessionId: this.session?.sessionId,
|
||||
error: error.message,
|
||||
});
|
||||
this.cleanupListeners();
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to spawn agent: ${error.message}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build CLI args for the agent based on its type and capabilities.
|
||||
|
||||
@@ -18,16 +18,22 @@ import {
|
||||
* Configuration for document generation
|
||||
*/
|
||||
export interface GenerationConfig {
|
||||
/** Agent type to use for generation */
|
||||
agentType: ToolType;
|
||||
/** Working directory for the agent */
|
||||
directoryPath: string;
|
||||
/** Project name from wizard */
|
||||
projectName: string;
|
||||
/** Full conversation history from project discovery */
|
||||
conversationHistory: WizardMessage[];
|
||||
/** Optional subfolder within Auto Run Docs (e.g., "Initiation") */
|
||||
subfolder?: string;
|
||||
/** Agent type to use for generation */
|
||||
agentType: ToolType;
|
||||
/** Working directory for the agent */
|
||||
directoryPath: string;
|
||||
/** Project name from wizard */
|
||||
projectName: string;
|
||||
/** Full conversation history from project discovery */
|
||||
conversationHistory: WizardMessage[];
|
||||
/** Optional subfolder within Auto Run Docs (e.g., "Initiation") */
|
||||
subfolder?: string;
|
||||
/** SSH remote configuration for remote agent execution */
|
||||
sessionSshRemoteConfig?: {
|
||||
enabled: boolean;
|
||||
remoteId: string | null;
|
||||
workingDirOverride?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1054,47 +1060,50 @@ class PhaseGenerator {
|
||||
// This is critical for packaged Electron apps where PATH may not include agent locations
|
||||
const commandToUse = agent.path || agent.command;
|
||||
|
||||
wizardDebugLogger.log('spawn', 'Calling process.spawn', {
|
||||
sessionId,
|
||||
toolType: config.agentType,
|
||||
cwd: config.directoryPath,
|
||||
command: commandToUse,
|
||||
agentPath: agent.path,
|
||||
agentCommand: agent.command,
|
||||
argsCount: argsForSpawn.length,
|
||||
promptLength: prompt.length,
|
||||
});
|
||||
window.maestro.process
|
||||
.spawn({
|
||||
sessionId,
|
||||
toolType: config.agentType,
|
||||
cwd: config.directoryPath,
|
||||
command: commandToUse,
|
||||
args: argsForSpawn,
|
||||
prompt,
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[PhaseGenerator] Agent spawned successfully');
|
||||
wizardDebugLogger.log('spawn', 'Agent spawned successfully', { sessionId });
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error('[PhaseGenerator] Spawn failed:', error.message);
|
||||
wizardDebugLogger.log('error', 'Spawn failed', {
|
||||
errorMessage: error.message,
|
||||
errorStack: error.stack,
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
this.cleanup();
|
||||
if (fileWatcherCleanup) {
|
||||
fileWatcherCleanup();
|
||||
}
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to spawn agent: ${error.message}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
wizardDebugLogger.log('spawn', 'Calling process.spawn', {
|
||||
sessionId,
|
||||
toolType: config.agentType,
|
||||
cwd: config.directoryPath,
|
||||
command: commandToUse,
|
||||
agentPath: agent.path,
|
||||
agentCommand: agent.command,
|
||||
argsCount: argsForSpawn.length,
|
||||
promptLength: prompt.length,
|
||||
hasRemoteSsh: !!config.sessionSshRemoteConfig?.enabled,
|
||||
remoteId: config.sessionSshRemoteConfig?.remoteId || null,
|
||||
});
|
||||
window.maestro.process
|
||||
.spawn({
|
||||
sessionId,
|
||||
toolType: config.agentType,
|
||||
cwd: config.directoryPath,
|
||||
command: commandToUse,
|
||||
args: argsForSpawn,
|
||||
prompt,
|
||||
sessionSshRemoteConfig: config.sessionSshRemoteConfig,
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[PhaseGenerator] Agent spawned successfully');
|
||||
wizardDebugLogger.log('spawn', 'Agent spawned successfully', { sessionId });
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error('[PhaseGenerator] Spawn failed:', error.message);
|
||||
wizardDebugLogger.log('error', 'Spawn failed', {
|
||||
errorMessage: error.message,
|
||||
errorStack: error.stack,
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
this.cleanup();
|
||||
if (fileWatcherCleanup) {
|
||||
fileWatcherCleanup();
|
||||
}
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to spawn agent: ${error.message}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read documents from the Auto Run Docs folder on disk
|
||||
|
||||
Reference in New Issue
Block a user