@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.
- package/CHANGELOG.md +49 -0
- package/README.md +2 -0
- package/agent/milestones/milestone-2-bug-fixes-production-readiness.md +159 -0
- package/agent/progress.yaml +92 -13
- package/agent/tasks/task-4-fix-read-file-not-found-bug.md +390 -0
- package/dist/server-factory.js +214 -20289
- package/dist/server-factory.js.map +4 -4
- package/dist/server.js +208 -20259
- package/dist/server.js.map +4 -4
- package/dist/utils/logger.d.ts +43 -0
- package/esbuild.build.js +1 -1
- package/esbuild.watch.js +1 -1
- package/package.json +1 -1
- package/src/server-factory.ts +45 -16
- package/src/server.ts +36 -18
- package/src/tools/acp-remote-execute-command.ts +11 -0
- package/src/tools/acp-remote-list-files.ts +7 -4
- package/src/tools/acp-remote-read-file.ts +6 -0
- package/src/tools/acp-remote-write-file.ts +6 -0
- package/src/utils/logger.ts +131 -0
- package/src/utils/ssh-connection.ts +59 -0
|
@@ -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
package/esbuild.watch.js
CHANGED
package/package.json
CHANGED
package/src/server-factory.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
+
logger.info('ACP MCP Server running on stdio');
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
main().catch((error) => {
|
|
82
|
-
|
|
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(`${
|
|
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
|
|
86
|
+
files.push(...subFiles); // Sub-files already have absolute paths
|
|
84
87
|
}
|
|
85
88
|
} else {
|
|
86
|
-
files.push(
|
|
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
|
}
|