@prmichaelsen/acp-mcp 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Logger utility for ACP MCP Server
3
+ * Provides structured logging with configurable log levels
4
+ */
5
+ export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
6
+ declare class Logger {
7
+ private level;
8
+ private enabled;
9
+ constructor();
10
+ private shouldLog;
11
+ private formatMessage;
12
+ error(message: string, data?: any): void;
13
+ warn(message: string, data?: any): void;
14
+ info(message: string, data?: any): void;
15
+ debug(message: string, data?: any): void;
16
+ trace(message: string, data?: any): void;
17
+ /**
18
+ * Log tool invocation with parameters
19
+ */
20
+ toolInvoked(toolName: string, params: any, userId?: string): void;
21
+ /**
22
+ * Log tool completion with result summary
23
+ */
24
+ toolCompleted(toolName: string, duration: number, resultSize?: number): void;
25
+ /**
26
+ * Log tool failure with error details
27
+ */
28
+ toolFailed(toolName: string, error: Error, params?: any): void;
29
+ /**
30
+ * Log SSH command execution
31
+ */
32
+ sshCommand(command: string, cwd?: string, timeout?: number): void;
33
+ /**
34
+ * Log SSH command result
35
+ */
36
+ sshCommandResult(exitCode: number, duration: number, stdoutSize: number, stderrSize: number): void;
37
+ /**
38
+ * Log file operation
39
+ */
40
+ fileOperation(operation: string, path: string, details?: any): void;
41
+ }
42
+ export declare const logger: Logger;
43
+ export {};
package/esbuild.build.js CHANGED
@@ -7,7 +7,7 @@ const sharedConfig = {
7
7
  target: 'node18',
8
8
  format: 'esm',
9
9
  sourcemap: true,
10
- external: ['@modelcontextprotocol/sdk'],
10
+ external: ['@modelcontextprotocol/sdk', 'ssh2'],
11
11
  };
12
12
 
13
13
  // Build standalone server
package/esbuild.watch.js CHANGED
@@ -6,7 +6,7 @@ const sharedConfig = {
6
6
  target: 'node18',
7
7
  format: 'esm',
8
8
  sourcemap: true,
9
- external: ['@modelcontextprotocol/sdk'],
9
+ external: ['@modelcontextprotocol/sdk', 'ssh2'],
10
10
  };
11
11
 
12
12
  // Watch standalone server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/acp-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for a remote machine MCP server that will be wrapped by /home/prmichaelsen/mcp-auth",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",
@@ -10,6 +10,7 @@ import { acpRemoteReadFileTool, handleAcpRemoteReadFile } from './tools/acp-remo
10
10
  import { acpRemoteWriteFileTool, handleAcpRemoteWriteFile } from './tools/acp-remote-write-file.js';
11
11
  import { ServerConfig } from './types/ssh-config.js';
12
12
  import { SSHConnectionManager } from './utils/ssh-connection.js';
13
+ import { logger } from './utils/logger.js';
13
14
 
14
15
  /**
15
16
  * Create an MCP server instance for a specific user with SSH configuration
@@ -19,11 +20,20 @@ import { SSHConnectionManager } from './utils/ssh-connection.js';
19
20
  * @returns Configured MCP Server instance
20
21
  */
21
22
  export async function createServer(serverConfig: ServerConfig): Promise<Server> {
23
+ logger.info('Creating server instance', { userId: serverConfig.userId });
24
+ logger.debug('SSH configuration', {
25
+ host: serverConfig.ssh.host,
26
+ port: serverConfig.ssh.port,
27
+ username: serverConfig.ssh.username,
28
+ });
29
+
22
30
  // Create SSH connection manager
23
31
  const sshConnection = new SSHConnectionManager(serverConfig.ssh);
24
32
 
25
33
  // Connect to remote server
26
34
  await sshConnection.connect();
35
+
36
+ logger.info('Server created successfully', { userId: serverConfig.userId });
27
37
 
28
38
  const server = new Server(
29
39
  {
@@ -38,34 +48,53 @@ export async function createServer(serverConfig: ServerConfig): Promise<Server>
38
48
  );
39
49
 
40
50
  // Register tools with SSH connection context
41
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
42
- tools: [acpRemoteListFilesTool, acpRemoteExecuteCommandTool, acpRemoteReadFileTool, acpRemoteWriteFileTool],
43
- }));
51
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
52
+ logger.debug('Tool discovery requested', { userId: serverConfig.userId });
53
+ const tools = [acpRemoteListFilesTool, acpRemoteExecuteCommandTool, acpRemoteReadFileTool, acpRemoteWriteFileTool];
54
+ logger.debug(`Returning ${tools.length} tools`, { tools: tools.map(t => t.name), userId: serverConfig.userId });
55
+ return { tools };
56
+ });
44
57
 
45
58
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
46
- if (request.params.name === 'acp_remote_list_files') {
47
- // Pass SSH connection to handler for remote operations
48
- return await handleAcpRemoteListFiles(request.params.arguments, sshConnection);
49
- }
50
- if (request.params.name === 'acp_remote_execute_command') {
51
- return await handleAcpRemoteExecuteCommand(request.params.arguments, sshConnection);
52
- }
53
- if (request.params.name === 'acp_remote_read_file') {
54
- return await handleAcpRemoteReadFile(request.params.arguments, sshConnection);
55
- }
56
- if (request.params.name === 'acp_remote_write_file') {
57
- return await handleAcpRemoteWriteFile(request.params.arguments, sshConnection);
59
+ const startTime = Date.now();
60
+ logger.toolInvoked(request.params.name, request.params.arguments, serverConfig.userId);
61
+
62
+ try {
63
+ let result;
64
+
65
+ if (request.params.name === 'acp_remote_list_files') {
66
+ // Pass SSH connection to handler for remote operations
67
+ result = await handleAcpRemoteListFiles(request.params.arguments, sshConnection);
68
+ } else if (request.params.name === 'acp_remote_execute_command') {
69
+ result = await handleAcpRemoteExecuteCommand(request.params.arguments, sshConnection);
70
+ } else if (request.params.name === 'acp_remote_read_file') {
71
+ result = await handleAcpRemoteReadFile(request.params.arguments, sshConnection);
72
+ } else if (request.params.name === 'acp_remote_write_file') {
73
+ result = await handleAcpRemoteWriteFile(request.params.arguments, sshConnection);
74
+ } else {
75
+ throw new Error(`Unknown tool: ${request.params.name}`);
76
+ }
77
+
78
+ const duration = Date.now() - startTime;
79
+ const resultSize = JSON.stringify(result).length;
80
+ logger.toolCompleted(request.params.name, duration, resultSize);
81
+
82
+ return result;
83
+ } catch (error) {
84
+ logger.toolFailed(request.params.name, error as Error, request.params.arguments);
85
+ throw error;
58
86
  }
59
- throw new Error(`Unknown tool: ${request.params.name}`);
60
87
  });
61
88
 
62
89
  // Handle server shutdown to cleanup SSH connection
63
90
  process.on('SIGINT', () => {
91
+ logger.info('Received SIGINT, shutting down', { userId: serverConfig.userId });
64
92
  sshConnection.disconnect();
65
93
  process.exit(0);
66
94
  });
67
95
 
68
96
  process.on('SIGTERM', () => {
97
+ logger.info('Received SIGTERM, shutting down', { userId: serverConfig.userId });
69
98
  sshConnection.disconnect();
70
99
  process.exit(0);
71
100
  });
package/src/server.ts CHANGED
@@ -12,6 +12,7 @@ import { acpRemoteExecuteCommandTool, handleAcpRemoteExecuteCommand } from './to
12
12
  import { acpRemoteReadFileTool, handleAcpRemoteReadFile } from './tools/acp-remote-read-file.js';
13
13
  import { acpRemoteWriteFileTool, handleAcpRemoteWriteFile } from './tools/acp-remote-write-file.js';
14
14
  import { SSHConnectionManager } from './utils/ssh-connection.js';
15
+ import { logger } from './utils/logger.js';
15
16
 
16
17
  async function main() {
17
18
  // Load SSH private key
@@ -42,29 +43,46 @@ async function main() {
42
43
  );
43
44
 
44
45
  // Register tools
45
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
46
- tools: [acpRemoteListFilesTool, acpRemoteExecuteCommandTool, acpRemoteReadFileTool, acpRemoteWriteFileTool],
47
- }));
46
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
47
+ logger.debug('Tool discovery requested');
48
+ const tools = [acpRemoteListFilesTool, acpRemoteExecuteCommandTool, acpRemoteReadFileTool, acpRemoteWriteFileTool];
49
+ logger.debug(`Returning ${tools.length} tools`, { tools: tools.map(t => t.name) });
50
+ return { tools };
51
+ });
48
52
 
49
53
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
50
- if (request.params.name === 'acp_remote_list_files') {
51
- return await handleAcpRemoteListFiles(request.params.arguments, sshConnection);
52
- }
53
- if (request.params.name === 'acp_remote_execute_command') {
54
- return await handleAcpRemoteExecuteCommand(request.params.arguments, sshConnection);
55
- }
56
- if (request.params.name === 'acp_remote_read_file') {
57
- return await handleAcpRemoteReadFile(request.params.arguments, sshConnection);
58
- }
59
- if (request.params.name === 'acp_remote_write_file') {
60
- return await handleAcpRemoteWriteFile(request.params.arguments, sshConnection);
54
+ const startTime = Date.now();
55
+ logger.toolInvoked(request.params.name, request.params.arguments);
56
+
57
+ try {
58
+ let result;
59
+
60
+ if (request.params.name === 'acp_remote_list_files') {
61
+ result = await handleAcpRemoteListFiles(request.params.arguments, sshConnection);
62
+ } else if (request.params.name === 'acp_remote_execute_command') {
63
+ result = await handleAcpRemoteExecuteCommand(request.params.arguments, sshConnection);
64
+ } else if (request.params.name === 'acp_remote_read_file') {
65
+ result = await handleAcpRemoteReadFile(request.params.arguments, sshConnection);
66
+ } else if (request.params.name === 'acp_remote_write_file') {
67
+ result = await handleAcpRemoteWriteFile(request.params.arguments, sshConnection);
68
+ } else {
69
+ throw new Error(`Unknown tool: ${request.params.name}`);
70
+ }
71
+
72
+ const duration = Date.now() - startTime;
73
+ const resultSize = JSON.stringify(result).length;
74
+ logger.toolCompleted(request.params.name, duration, resultSize);
75
+
76
+ return result;
77
+ } catch (error) {
78
+ logger.toolFailed(request.params.name, error as Error, request.params.arguments);
79
+ throw error;
61
80
  }
62
- throw new Error(`Unknown tool: ${request.params.name}`);
63
81
  });
64
82
 
65
83
  // Handle shutdown to cleanup SSH connection
66
84
  const cleanup = () => {
67
- console.error('Shutting down...');
85
+ logger.info('Shutting down server');
68
86
  sshConnection.disconnect();
69
87
  process.exit(0);
70
88
  };
@@ -75,10 +93,10 @@ async function main() {
75
93
  // Start server
76
94
  const transport = new StdioServerTransport();
77
95
  await server.connect(transport);
78
- console.error('ACP MCP Server running on stdio');
96
+ logger.info('ACP MCP Server running on stdio');
79
97
  }
80
98
 
81
99
  main().catch((error) => {
82
- console.error('Server error:', error);
100
+ logger.error('Server startup failed', { error: error.message, stack: error.stack });
83
101
  process.exit(1);
84
102
  });
@@ -1,5 +1,6 @@
1
1
  import { Tool } from '@modelcontextprotocol/sdk/types.js';
2
2
  import { SSHConnectionManager } from '../utils/ssh-connection.js';
3
+ import { logger } from '../utils/logger.js';
3
4
 
4
5
  export const acpRemoteExecuteCommandTool: Tool = {
5
6
  name: 'acp_remote_execute_command',
@@ -51,12 +52,21 @@ export async function handleAcpRemoteExecuteCommand(
51
52
  ): Promise<{ content: Array<{ type: string; text: string }> }> {
52
53
  const { command, cwd, timeout = 30 } = args as ExecuteCommandArgs;
53
54
 
55
+ logger.debug('Executing remote command', { command, cwd, timeout });
56
+
54
57
  try {
55
58
  // Build command with working directory if specified
56
59
  const fullCommand = cwd ? `cd ${cwd} && ${command}` : command;
57
60
 
58
61
  const result = await sshConnection.execWithTimeout(fullCommand, timeout);
59
62
 
63
+ logger.debug('Command execution result', {
64
+ exitCode: result.exitCode,
65
+ timedOut: result.timedOut,
66
+ stdoutLength: result.stdout.length,
67
+ stderrLength: result.stderr.length,
68
+ });
69
+
60
70
  // Format output as JSON for structured response
61
71
  const output: ExecuteCommandResult = {
62
72
  stdout: result.stdout,
@@ -75,6 +85,7 @@ export async function handleAcpRemoteExecuteCommand(
75
85
  };
76
86
  } catch (error) {
77
87
  const errorMessage = error instanceof Error ? error.message : String(error);
88
+ logger.error('Command execution error', { command, error: errorMessage });
78
89
  return {
79
90
  content: [
80
91
  {
@@ -65,6 +65,7 @@ export async function handleAcpRemoteListFiles(
65
65
 
66
66
  /**
67
67
  * Recursively list files in a remote directory via SSH
68
+ * Returns absolute paths for compatibility with acp_remote_read_file
68
69
  */
69
70
  async function listRemoteFiles(
70
71
  ssh: SSHConnectionManager,
@@ -75,15 +76,17 @@ async function listRemoteFiles(
75
76
  const files: string[] = [];
76
77
 
77
78
  for (const entry of entries) {
79
+ // Construct absolute path by combining dirPath with entry name
80
+ const fullPath = `${dirPath}/${entry.name}`.replace(/\/+/g, '/');
81
+
78
82
  if (entry.isDirectory) {
79
- files.push(`${entry.name}/`);
83
+ files.push(`${fullPath}/`); // Return absolute path with trailing slash
80
84
  if (recursive) {
81
- const fullPath = `${dirPath}/${entry.name}`.replace(/\/+/g, '/');
82
85
  const subFiles = await listRemoteFiles(ssh, fullPath, recursive);
83
- files.push(...subFiles.map(f => `${entry.name}/${f}`));
86
+ files.push(...subFiles); // Sub-files already have absolute paths
84
87
  }
85
88
  } else {
86
- files.push(entry.name);
89
+ files.push(fullPath); // Return absolute path
87
90
  }
88
91
  }
89
92
 
@@ -1,5 +1,6 @@
1
1
  import { Tool } from '@modelcontextprotocol/sdk/types.js';
2
2
  import { SSHConnectionManager } from '../utils/ssh-connection.js';
3
+ import { logger } from '../utils/logger.js';
3
4
 
4
5
  export const acpRemoteReadFileTool: Tool = {
5
6
  name: 'acp_remote_read_file',
@@ -52,9 +53,13 @@ export async function handleAcpRemoteReadFile(
52
53
  ): Promise<{ content: Array<{ type: string; text: string }> }> {
53
54
  const { path, encoding = 'utf-8', maxSize = 1048576 } = args as ReadFileArgs;
54
55
 
56
+ logger.debug('Reading remote file', { path, encoding, maxSize });
57
+
55
58
  try {
56
59
  const result = await sshConnection.readFile(path, encoding, maxSize);
57
60
 
61
+ logger.debug('File read successful', { path, size: result.size });
62
+
58
63
  const output: ReadFileResult = {
59
64
  content: result.content,
60
65
  size: result.size,
@@ -71,6 +76,7 @@ export async function handleAcpRemoteReadFile(
71
76
  };
72
77
  } catch (error) {
73
78
  const errorMessage = error instanceof Error ? error.message : String(error);
79
+ logger.error('File read error', { path, error: errorMessage });
74
80
  return {
75
81
  content: [
76
82
  {
@@ -1,5 +1,6 @@
1
1
  import { Tool } from '@modelcontextprotocol/sdk/types.js';
2
2
  import { SSHConnectionManager } from '../utils/ssh-connection.js';
3
+ import { logger } from '../utils/logger.js';
3
4
 
4
5
  export const acpRemoteWriteFileTool: Tool = {
5
6
  name: 'acp_remote_write_file',
@@ -62,6 +63,8 @@ export async function handleAcpRemoteWriteFile(
62
63
  ): Promise<{ content: Array<{ type: string; text: string }> }> {
63
64
  const { path, content, encoding = 'utf-8', createDirs = false, backup = false } = args as WriteFileArgs;
64
65
 
66
+ logger.debug('Writing remote file', { path, contentSize: content.length, encoding, createDirs, backup });
67
+
65
68
  try {
66
69
  const result = await sshConnection.writeFile(path, content, {
67
70
  encoding,
@@ -69,6 +72,8 @@ export async function handleAcpRemoteWriteFile(
69
72
  backup,
70
73
  });
71
74
 
75
+ logger.debug('File write successful', { path, bytesWritten: result.bytesWritten, backupPath: result.backupPath });
76
+
72
77
  const output: WriteFileResult = {
73
78
  success: result.success,
74
79
  bytesWritten: result.bytesWritten,
@@ -85,6 +90,7 @@ export async function handleAcpRemoteWriteFile(
85
90
  };
86
91
  } catch (error) {
87
92
  const errorMessage = error instanceof Error ? error.message : String(error);
93
+ logger.error('File write error', { path, error: errorMessage });
88
94
  return {
89
95
  content: [
90
96
  {
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Logger utility for ACP MCP Server
3
+ * Provides structured logging with configurable log levels
4
+ */
5
+
6
+ export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
7
+
8
+ const LOG_LEVELS: Record<LogLevel, number> = {
9
+ error: 0,
10
+ warn: 1,
11
+ info: 2,
12
+ debug: 3,
13
+ trace: 4,
14
+ };
15
+
16
+ class Logger {
17
+ private level: LogLevel;
18
+ private enabled: boolean;
19
+
20
+ constructor() {
21
+ // Read configuration from environment variables
22
+ this.level = (process.env.ACP_MCP_LOG_LEVEL as LogLevel) || 'info';
23
+ this.enabled = process.env.ACP_MCP_DEBUG === 'true' || process.env.NODE_ENV === 'development';
24
+ }
25
+
26
+ private shouldLog(level: LogLevel): boolean {
27
+ if (!this.enabled && level !== 'error' && level !== 'warn') {
28
+ return false;
29
+ }
30
+ return LOG_LEVELS[level] <= LOG_LEVELS[this.level];
31
+ }
32
+
33
+ private formatMessage(level: LogLevel, message: string, data?: any): string {
34
+ const timestamp = new Date().toISOString();
35
+ const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
36
+
37
+ if (data !== undefined) {
38
+ const dataStr = typeof data === 'object' ? JSON.stringify(data, null, 2) : String(data);
39
+ return `${prefix} ${message}\n${dataStr}`;
40
+ }
41
+
42
+ return `${prefix} ${message}`;
43
+ }
44
+
45
+ error(message: string, data?: any): void {
46
+ if (this.shouldLog('error')) {
47
+ console.error(this.formatMessage('error', message, data));
48
+ }
49
+ }
50
+
51
+ warn(message: string, data?: any): void {
52
+ if (this.shouldLog('warn')) {
53
+ console.error(this.formatMessage('warn', message, data));
54
+ }
55
+ }
56
+
57
+ info(message: string, data?: any): void {
58
+ if (this.shouldLog('info')) {
59
+ console.error(this.formatMessage('info', message, data));
60
+ }
61
+ }
62
+
63
+ debug(message: string, data?: any): void {
64
+ if (this.shouldLog('debug')) {
65
+ console.error(this.formatMessage('debug', message, data));
66
+ }
67
+ }
68
+
69
+ trace(message: string, data?: any): void {
70
+ if (this.shouldLog('trace')) {
71
+ console.error(this.formatMessage('trace', message, data));
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Log tool invocation with parameters
77
+ */
78
+ toolInvoked(toolName: string, params: any, userId?: string): void {
79
+ this.info(`Tool invoked: ${toolName}`);
80
+ this.debug('Tool parameters', { tool: toolName, params, userId });
81
+ }
82
+
83
+ /**
84
+ * Log tool completion with result summary
85
+ */
86
+ toolCompleted(toolName: string, duration: number, resultSize?: number): void {
87
+ this.info(`Tool completed: ${toolName}`);
88
+ this.debug('Tool performance', { tool: toolName, duration: `${duration}ms`, resultSize });
89
+ }
90
+
91
+ /**
92
+ * Log tool failure with error details
93
+ */
94
+ toolFailed(toolName: string, error: Error, params?: any): void {
95
+ this.error(`Tool execution failed: ${toolName}`, {
96
+ tool: toolName,
97
+ error: error.message,
98
+ stack: error.stack,
99
+ params,
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Log SSH command execution
105
+ */
106
+ sshCommand(command: string, cwd?: string, timeout?: number): void {
107
+ this.debug('Executing SSH command', { command, cwd, timeout });
108
+ }
109
+
110
+ /**
111
+ * Log SSH command result
112
+ */
113
+ sshCommandResult(exitCode: number, duration: number, stdoutSize: number, stderrSize: number): void {
114
+ this.debug('SSH command completed', {
115
+ exitCode,
116
+ duration: `${duration}ms`,
117
+ stdout: `${stdoutSize} bytes`,
118
+ stderr: `${stderrSize} bytes`,
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Log file operation
124
+ */
125
+ fileOperation(operation: string, path: string, details?: any): void {
126
+ this.info(`File operation: ${operation}`, { path, ...details });
127
+ }
128
+ }
129
+
130
+ // Export singleton instance
131
+ export const logger = new Logger();
@@ -1,5 +1,6 @@
1
1
  import { Client, SFTPWrapper } from 'ssh2';
2
2
  import { SSHConfig } from '../types/ssh-config.js';
3
+ import { logger } from './logger.js';
3
4
 
4
5
  /**
5
6
  * SSH Connection Manager
@@ -20,16 +21,31 @@ export class SSHConnectionManager {
20
21
  */
21
22
  async connect(): Promise<void> {
22
23
  if (this.connected) {
24
+ logger.debug('SSH connection already established');
23
25
  return;
24
26
  }
25
27
 
28
+ logger.info('Connecting to SSH server', {
29
+ host: this.config.host,
30
+ port: this.config.port || 22,
31
+ username: this.config.username,
32
+ });
33
+
26
34
  return new Promise((resolve, reject) => {
27
35
  this.client
28
36
  .on('ready', () => {
29
37
  this.connected = true;
38
+ logger.info('SSH connection established', {
39
+ host: this.config.host,
40
+ username: this.config.username,
41
+ });
30
42
  resolve();
31
43
  })
32
44
  .on('error', (err) => {
45
+ logger.error('SSH connection failed', {
46
+ host: this.config.host,
47
+ error: err.message,
48
+ });
33
49
  reject(err);
34
50
  })
35
51
  .connect({
@@ -88,6 +104,9 @@ export class SSHConnectionManager {
88
104
  await this.connect();
89
105
  }
90
106
 
107
+ const startTime = Date.now();
108
+ logger.sshCommand(command, undefined, timeoutSeconds);
109
+
91
110
  const execPromise = new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve, reject) => {
92
111
  this.client.exec(command, (err, stream) => {
93
112
  if (err) {
@@ -100,6 +119,8 @@ export class SSHConnectionManager {
100
119
 
101
120
  stream
102
121
  .on('close', (code: number) => {
122
+ const duration = Date.now() - startTime;
123
+ logger.sshCommandResult(code, duration, stdout.length, stderr.length);
103
124
  resolve({ stdout, stderr, exitCode: code });
104
125
  })
105
126
  .on('data', (data: Buffer) => {
@@ -122,6 +143,7 @@ export class SSHConnectionManager {
122
143
  return { ...result, timedOut: false };
123
144
  } catch (error) {
124
145
  if (error instanceof Error && error.message === 'Command execution timed out') {
146
+ logger.warn('SSH command timed out', { command, timeout: timeoutSeconds });
125
147
  return {
126
148
  stdout: '',
127
149
  stderr: 'Command execution timed out',
@@ -129,6 +151,10 @@ export class SSHConnectionManager {
129
151
  timedOut: true,
130
152
  };
131
153
  }
154
+ logger.error('SSH command execution failed', {
155
+ command,
156
+ error: error instanceof Error ? error.message : String(error),
157
+ });
132
158
  throw error;
133
159
  }
134
160
  }
@@ -183,17 +209,24 @@ export class SSHConnectionManager {
183
209
  encoding: string = 'utf-8',
184
210
  maxSize: number = 1048576
185
211
  ): Promise<{ content: string; size: number; encoding: string }> {
212
+ const startTime = Date.now();
213
+ logger.fileOperation('read', path, { encoding, maxSize });
214
+
186
215
  const sftp = await this.getSFTP();
187
216
 
188
217
  return new Promise((resolve, reject) => {
189
218
  // First, get file stats to check size
190
219
  sftp.stat(path, (err, stats) => {
191
220
  if (err) {
221
+ logger.error('File stat failed', { path, error: err.message });
192
222
  reject(new Error(`File not found or inaccessible: ${path}`));
193
223
  return;
194
224
  }
195
225
 
226
+ logger.debug('File stat retrieved', { path, size: stats.size });
227
+
196
228
  if (stats.size > maxSize) {
229
+ logger.warn('File too large', { path, size: stats.size, maxSize });
197
230
  reject(new Error(`File too large: ${stats.size} bytes (max: ${maxSize} bytes)`));
198
231
  return;
199
232
  }
@@ -201,10 +234,14 @@ export class SSHConnectionManager {
201
234
  // Read file contents
202
235
  sftp.readFile(path, { encoding: encoding as BufferEncoding }, (err, data) => {
203
236
  if (err) {
237
+ logger.error('File read failed', { path, error: err.message });
204
238
  reject(new Error(`Failed to read file: ${err.message}`));
205
239
  return;
206
240
  }
207
241
 
242
+ const duration = Date.now() - startTime;
243
+ logger.debug('File read completed', { path, size: stats.size, duration: `${duration}ms` });
244
+
208
245
  resolve({
209
246
  content: data.toString(),
210
247
  size: stats.size,
@@ -228,6 +265,15 @@ export class SSHConnectionManager {
228
265
  } = {}
229
266
  ): Promise<{ success: boolean; bytesWritten: number; backupPath?: string }> {
230
267
  const { encoding = 'utf-8', createDirs = false, backup = false } = options;
268
+ const startTime = Date.now();
269
+
270
+ logger.fileOperation('write', path, {
271
+ contentSize: content.length,
272
+ encoding,
273
+ createDirs,
274
+ backup,
275
+ });
276
+
231
277
  const sftp = await this.getSFTP();
232
278
 
233
279
  return new Promise((resolve, reject) => {
@@ -264,10 +310,19 @@ export class SSHConnectionManager {
264
310
  // Rename temp file to target (atomic operation)
265
311
  sftp.rename(tempPath, path, (err) => {
266
312
  if (err) {
313
+ logger.error('File rename failed', { tempPath, path, error: err.message });
267
314
  reject(new Error(`Failed to rename temp file: ${err.message}`));
268
315
  return;
269
316
  }
270
317
 
318
+ const duration = Date.now() - startTime;
319
+ logger.debug('File write completed', {
320
+ path,
321
+ bytesWritten: buffer.length,
322
+ duration: `${duration}ms`,
323
+ backupPath,
324
+ });
325
+
271
326
  resolve({
272
327
  success: true,
273
328
  bytesWritten: buffer.length,
@@ -294,6 +349,10 @@ export class SSHConnectionManager {
294
349
  */
295
350
  disconnect(): void {
296
351
  if (this.connected) {
352
+ logger.info('Disconnecting from SSH server', {
353
+ host: this.config.host,
354
+ username: this.config.username,
355
+ });
297
356
  this.client.end();
298
357
  this.connected = false;
299
358
  }