@codesys/mcp-toolkit 1.0.6 → 1.0.8
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/direct-codesys-mcp.js +150 -0
- package/dist/bin.js +15 -6
- package/dist/codesys_interop.js +104 -64
- package/dist/server.js +899 -884
- package/package.json +45 -47
- package/test-direct.js +0 -40
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { exec } = require('child_process');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
7
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
8
|
+
const { z } = require('zod');
|
|
9
|
+
|
|
10
|
+
console.error("Starting standalone CODESYS MCP server...");
|
|
11
|
+
|
|
12
|
+
// Create a simple MCP server
|
|
13
|
+
const server = new McpServer({
|
|
14
|
+
name: "Standalone CODESYS MCP Server",
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
capabilities: { resources: {}, tools: {} }
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Direct implementation of executeCodesysScript
|
|
20
|
+
async function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName) {
|
|
21
|
+
// Validate inputs
|
|
22
|
+
if (!codesysExePath) throw new Error('CODESYS executable path was not provided');
|
|
23
|
+
if (!codesysProfileName) throw new Error('CODESYS profile name was not provided');
|
|
24
|
+
|
|
25
|
+
console.error(`DIRECT: Using CODESYS Path: ${codesysExePath}`);
|
|
26
|
+
console.error(`DIRECT: Using CODESYS Profile: ${codesysProfileName}`);
|
|
27
|
+
|
|
28
|
+
// Create temp script file
|
|
29
|
+
const tempDir = os.tmpdir();
|
|
30
|
+
const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).slice(2, 9)}.py`;
|
|
31
|
+
const tempFilePath = path.join(tempDir, tempFileName);
|
|
32
|
+
|
|
33
|
+
let output = '';
|
|
34
|
+
let success = false;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Write script to temp file
|
|
38
|
+
fs.writeFileSync(tempFilePath, scriptContent, 'latin1');
|
|
39
|
+
console.error(`DIRECT: Wrote script to: ${tempFilePath}`);
|
|
40
|
+
|
|
41
|
+
// Build command with proper quoting
|
|
42
|
+
const commandWithArgs = `"${codesysExePath}" --profile="${codesysProfileName}" --noUI --runscript="${tempFilePath}"`;
|
|
43
|
+
console.error(`DIRECT: Command: ${commandWithArgs}`);
|
|
44
|
+
|
|
45
|
+
// Execute using exec
|
|
46
|
+
success = await new Promise((resolve) => {
|
|
47
|
+
exec(commandWithArgs, (error, stdout, stderr) => {
|
|
48
|
+
if (stdout) output += stdout;
|
|
49
|
+
if (stderr) output += stderr;
|
|
50
|
+
|
|
51
|
+
console.error(`DIRECT: stdout: ${stdout.slice(0, 200)}${stdout.length > 200 ? '...' : ''}`);
|
|
52
|
+
if (stderr) console.error(`DIRECT: stderr: ${stderr}`);
|
|
53
|
+
|
|
54
|
+
if (error) {
|
|
55
|
+
console.error(`DIRECT: Error: ${error.message}`);
|
|
56
|
+
resolve(false);
|
|
57
|
+
} else {
|
|
58
|
+
resolve(true);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Check for success/error markers in output
|
|
64
|
+
if (output.includes('SCRIPT_SUCCESS')) {
|
|
65
|
+
success = true;
|
|
66
|
+
} else if (output.includes('SCRIPT_ERROR')) {
|
|
67
|
+
success = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error(`DIRECT: Exception: ${err.message}`);
|
|
72
|
+
output += `\nError: ${err.message}`;
|
|
73
|
+
success = false;
|
|
74
|
+
} finally {
|
|
75
|
+
// Clean up temp file
|
|
76
|
+
try {
|
|
77
|
+
fs.unlinkSync(tempFilePath);
|
|
78
|
+
console.error(`DIRECT: Deleted temp file: ${tempFilePath}`);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error(`DIRECT: Failed to delete temp file: ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { success, output };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Implement a basic check_status tool
|
|
88
|
+
server.tool("check_status", {}, async () => {
|
|
89
|
+
try {
|
|
90
|
+
const codesysPath = process.argv[2] || "C:\\Program Files\\CODESYS 3.5.21.0\\CODESYS\\Common\\CODESYS.exe";
|
|
91
|
+
const profileName = process.argv[3] || "CODESYS V3.5 SP21";
|
|
92
|
+
|
|
93
|
+
console.error(`DIRECT: Check Status with ${codesysPath} and ${profileName}`);
|
|
94
|
+
|
|
95
|
+
const scriptContent = `
|
|
96
|
+
import sys, scriptengine as script_engine, os
|
|
97
|
+
try:
|
|
98
|
+
print("Python script running")
|
|
99
|
+
print("Checking CODESYS status...")
|
|
100
|
+
scripting_ok = True
|
|
101
|
+
primary_project = script_engine.projects.primary
|
|
102
|
+
print("Primary project: " + str(primary_project))
|
|
103
|
+
print("SCRIPT_SUCCESS: Status check completed")
|
|
104
|
+
sys.exit(0)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print("Error: " + str(e))
|
|
107
|
+
print("SCRIPT_ERROR: " + str(e))
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
const result = await executeCodesysScript(scriptContent, codesysPath, profileName);
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: `Status check ${result.success ? 'succeeded' : 'failed'}.\nOutput:\n${result.output}` }],
|
|
114
|
+
isError: !result.success
|
|
115
|
+
};
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error(`DIRECT: Tool error: ${err.message}`);
|
|
118
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Start the server
|
|
123
|
+
async function startServer() {
|
|
124
|
+
try {
|
|
125
|
+
const transport = new StdioServerTransport();
|
|
126
|
+
console.error("DIRECT: Connecting server via stdio...");
|
|
127
|
+
await server.connect(transport);
|
|
128
|
+
console.error("DIRECT: Server connected and listening");
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error("DIRECT: Failed to start server:", error);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Show args
|
|
136
|
+
console.error(`DIRECT: Args: ${JSON.stringify(process.argv)}`);
|
|
137
|
+
|
|
138
|
+
// Start the server
|
|
139
|
+
startServer();
|
|
140
|
+
|
|
141
|
+
// Handle shutdown
|
|
142
|
+
process.on('SIGINT', () => {
|
|
143
|
+
console.error('\nDIRECT: SIGINT received, shutting down...');
|
|
144
|
+
process.exit(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
process.on('SIGTERM', () => {
|
|
148
|
+
console.error('\nDIRECT: SIGTERM received, shutting down...');
|
|
149
|
+
process.exit(0);
|
|
150
|
+
});
|
package/dist/bin.js
CHANGED
|
@@ -18,13 +18,22 @@ commander_1.program
|
|
|
18
18
|
.option('-p, --codesys-profile <profile>', 'CODESYS profile name', 'CODESYS V3.5 SP21')
|
|
19
19
|
.parse(process.argv);
|
|
20
20
|
const options = commander_1.program.opts();
|
|
21
|
-
//
|
|
21
|
+
// Enhanced logging for debugging purposes
|
|
22
|
+
console.error(`========================================`);
|
|
22
23
|
console.error(`Starting CODESYS MCP Server v${version}`);
|
|
23
|
-
console.error(`
|
|
24
|
-
console.error(`
|
|
25
|
-
|
|
24
|
+
console.error(`CODESYS path: ${options.codesysPath}`);
|
|
25
|
+
console.error(`CODESYS profile: ${options.codesysProfile}`);
|
|
26
|
+
console.error(`Using Node.js ${process.version}`);
|
|
27
|
+
console.error(`Current directory: ${process.cwd()}`);
|
|
28
|
+
console.error(`========================================`);
|
|
29
|
+
// Pass properly sanitized command line arguments
|
|
30
|
+
const codesysPath = options.codesysPath.trim();
|
|
31
|
+
const codesysProfile = options.codesysProfile.trim();
|
|
26
32
|
// The server.js file expects these as positional parameters
|
|
27
|
-
process.argv[2] =
|
|
28
|
-
process.argv[3] =
|
|
33
|
+
process.argv[2] = codesysPath;
|
|
34
|
+
process.argv[3] = codesysProfile;
|
|
35
|
+
// Also set them as environment variables as a backup mechanism
|
|
36
|
+
process.env.CODESYS_PATH = codesysPath;
|
|
37
|
+
process.env.CODESYS_PROFILE = codesysProfile;
|
|
29
38
|
// Import and start the server
|
|
30
39
|
import('./server.js');
|
package/dist/codesys_interop.js
CHANGED
|
@@ -43,7 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.executeCodesysScript = executeCodesysScript;
|
|
46
|
-
const child_process_1 = require("child_process");
|
|
46
|
+
const child_process_1 = require("child_process"); // Use spawn directly
|
|
47
47
|
const promises_1 = require("fs/promises");
|
|
48
48
|
const path = __importStar(require("path"));
|
|
49
49
|
const os = __importStar(require("os"));
|
|
@@ -52,6 +52,7 @@ const SCRIPT_SUCCESS_MARKER = 'SCRIPT_SUCCESS';
|
|
|
52
52
|
const SCRIPT_ERROR_MARKER = 'SCRIPT_ERROR';
|
|
53
53
|
/**
|
|
54
54
|
* Executes a CODESYS Python script using the command line interface.
|
|
55
|
+
* Uses spawn for better handling of paths with spaces.
|
|
55
56
|
*
|
|
56
57
|
* @param scriptContent The Python script code to execute.
|
|
57
58
|
* @param codesysExePath The full path to the CODESYS.exe executable.
|
|
@@ -69,58 +70,95 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
|
|
|
69
70
|
}
|
|
70
71
|
// Create a temporary file for the script
|
|
71
72
|
const tempDir = os.tmpdir();
|
|
72
|
-
|
|
73
|
+
// Ensure temp file name is safe for command line (though spawn handles args better)
|
|
74
|
+
const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
|
|
73
75
|
const tempFilePath = path.join(tempDir, tempFileName);
|
|
74
76
|
let output = '';
|
|
75
77
|
let success = false;
|
|
78
|
+
let exitCode = null;
|
|
76
79
|
try {
|
|
77
80
|
// Normalize line endings to LF (\n)
|
|
78
81
|
const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
|
|
79
82
|
// Write the script content to the temporary file using latin1 encoding
|
|
80
|
-
yield (0, promises_1.writeFile)(tempFilePath, normalizedScriptContent, 'latin1');
|
|
81
|
-
process.stderr.write(`Temporary script written to: ${tempFilePath} with latin1 encoding and LF line endings\n`);
|
|
82
|
-
//
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
83
|
+
yield (0, promises_1.writeFile)(tempFilePath, normalizedScriptContent, 'latin1');
|
|
84
|
+
process.stderr.write(`Temporary script written to: ${tempFilePath} with latin1 encoding and LF line endings\n`);
|
|
85
|
+
// --- Use spawn ---
|
|
86
|
+
const command = codesysExePath; // The executable itself
|
|
87
|
+
const args = [
|
|
88
|
+
`--profile=${codesysProfileName}`, // Format expected by CODESYS.exe
|
|
89
|
+
'--noUI',
|
|
90
|
+
`--runscript=${tempFilePath}` // Format expected by CODESYS.exe
|
|
91
|
+
// If CODESYS requires quotes *around* the profile/script path *value*, adjust like:
|
|
92
|
+
// `--profile="${codesysProfileName}"`,
|
|
93
|
+
// `--runscript="${tempFilePath}"`,
|
|
94
|
+
// But usually, spawn handles the argument separation correctly without needing inner quotes unless the *program itself* requires them.
|
|
95
|
+
// Test without inner quotes first.
|
|
96
|
+
];
|
|
97
|
+
process.stderr.write(`Spawning: ${command} with args: ${JSON.stringify(args)}\n`);
|
|
98
|
+
const spawnResult = yield new Promise((resolve, reject) => {
|
|
99
|
+
let stdoutData = '';
|
|
100
|
+
let stderrData = '';
|
|
101
|
+
// Spawn the process. Detached:false is default. Timeout via AbortController recommended for spawn.
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timeoutSignal = controller.signal;
|
|
104
|
+
const timeoutDuration = 30000; // 30 seconds
|
|
105
|
+
const childProcess = (0, child_process_1.spawn)(command, args, {
|
|
106
|
+
windowsHide: true, // Hides the console window on Windows
|
|
107
|
+
signal: timeoutSignal // Use AbortSignal for timeout
|
|
108
|
+
});
|
|
109
|
+
const timeoutId = setTimeout(() => {
|
|
110
|
+
process.stderr.write(`TIMEOUT: CODESYS process timed out after ${timeoutDuration / 1000} seconds\n`);
|
|
111
|
+
controller.abort(); // Abort the process via the signal
|
|
112
|
+
}, timeoutDuration);
|
|
113
|
+
childProcess.stdout.on('data', (data) => {
|
|
114
|
+
const chunk = data.toString();
|
|
115
|
+
stdoutData += chunk;
|
|
116
|
+
process.stderr.write(`CODESYS stdout chunk: ${chunk.length > 100 ? chunk.substring(0, 100) + '...' : chunk}\n`);
|
|
117
|
+
});
|
|
118
|
+
childProcess.stderr.on('data', (data) => {
|
|
119
|
+
const chunk = data.toString();
|
|
120
|
+
stderrData += chunk;
|
|
121
|
+
process.stderr.write(`CODESYS stderr chunk: ${chunk}\n`); // Log stderr more verbosely
|
|
122
|
+
});
|
|
123
|
+
childProcess.on('error', (error) => {
|
|
93
124
|
var _a;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
process.stderr.write(`CODESYS process error: ${error.message}\n`);
|
|
106
|
-
resolve((_a = error.code) !== null && _a !== void 0 ? _a : 1); // Return error code
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
// Command succeeded
|
|
110
|
-
resolve(0);
|
|
111
|
-
}
|
|
125
|
+
clearTimeout(timeoutId); // Clear timeout on explicit error
|
|
126
|
+
// This usually means the process couldn't be spawned (e.g., EACCES, ENOENT)
|
|
127
|
+
process.stderr.write(`CODESYS spawn error: ${error.message}\n`);
|
|
128
|
+
// Don't reject here, resolve with error info like exec did
|
|
129
|
+
resolve({ code: (_a = error.errno) !== null && _a !== void 0 ? _a : 1, output: stdoutData + stderrData + `\nSpawn Error: ${error.message}` });
|
|
130
|
+
});
|
|
131
|
+
childProcess.on('close', (code) => {
|
|
132
|
+
clearTimeout(timeoutId); // Clear timeout on successful close
|
|
133
|
+
process.stderr.write(`CODESYS process closed with code: ${code}\n`);
|
|
134
|
+
const combinedOutput = stdoutData + stderrData;
|
|
135
|
+
resolve({ code: code, output: combinedOutput });
|
|
112
136
|
});
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
process.stderr.write(
|
|
116
|
-
childProcess.
|
|
117
|
-
|
|
118
|
-
|
|
137
|
+
// Handle the timeout signal specifically
|
|
138
|
+
timeoutSignal.addEventListener('abort', () => {
|
|
139
|
+
process.stderr.write('Abort signal received, killing process if still running.\n');
|
|
140
|
+
if (!childProcess.killed) {
|
|
141
|
+
childProcess.kill('SIGTERM'); // Attempt graceful kill first
|
|
142
|
+
// Optionally force kill after a short delay if SIGTERM doesn't work
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
if (!childProcess.killed) {
|
|
145
|
+
process.stderr.write('SIGTERM ineffective, sending SIGKILL.\n');
|
|
146
|
+
childProcess.kill('SIGKILL');
|
|
147
|
+
}
|
|
148
|
+
}, 2000); // 2 second grace period
|
|
149
|
+
}
|
|
150
|
+
// Resolve with timeout information. The 'close' event might still fire later with a specific code.
|
|
151
|
+
// We already logged the timeout, let the 'close' handler report the final code.
|
|
152
|
+
// If 'close' doesn't fire after kill, it might indicate a deeper issue.
|
|
153
|
+
// We could resolve here with a timeout indicator, but letting 'close' handle it is cleaner if it works.
|
|
154
|
+
// If 'close' never fires after abort/kill, the promise might hang. Let's add a failsafe resolve in the listener.
|
|
155
|
+
// Revised: Resolve directly on abort to ensure promise completion.
|
|
156
|
+
resolve({ code: null, output: stdoutData + stderrData + "\nTIMEOUT: CODESYS process aborted due to timeout." });
|
|
157
|
+
}, { once: true }); // Ensure listener is called only once
|
|
119
158
|
});
|
|
120
|
-
|
|
159
|
+
output = spawnResult.output;
|
|
160
|
+
exitCode = spawnResult.code; // Can be null if process killed before exit code retrieved
|
|
121
161
|
// Determine success based on exit code and output markers
|
|
122
|
-
// Note: CODESYS scripting might return exit code 0 even if the script had logical errors.
|
|
123
|
-
// Relying on specific output markers is more robust.
|
|
124
162
|
if (output.includes(SCRIPT_SUCCESS_MARKER)) {
|
|
125
163
|
success = true;
|
|
126
164
|
}
|
|
@@ -128,48 +166,50 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
|
|
|
128
166
|
success = false;
|
|
129
167
|
}
|
|
130
168
|
else {
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
169
|
+
// Fallback logic (handle null exit code)
|
|
170
|
+
if (exitCode === 0) {
|
|
171
|
+
console.warn("Warning: CODESYS script finished with exit code 0 but no SCRIPT_SUCCESS marker found.");
|
|
172
|
+
success = !output.includes(SCRIPT_ERROR_MARKER); // Assume success if exit code 0 and no error marker
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
success = false;
|
|
176
|
+
const exitMsg = exitCode !== null ? `exit code ${exitCode}` : 'signal or unknown reason';
|
|
135
177
|
if (!output.includes(SCRIPT_ERROR_MARKER)) {
|
|
136
|
-
output += `\nSCRIPT_ERROR: Process exited with
|
|
178
|
+
output += `\nSCRIPT_ERROR: Process exited with ${exitMsg}. Check CODESYS stderr output above for details.`;
|
|
137
179
|
}
|
|
138
180
|
else {
|
|
139
|
-
output += `\nSCRIPT_ERROR: Process exited with
|
|
181
|
+
output += `\nSCRIPT_ERROR: Process exited with ${exitMsg}.`;
|
|
140
182
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
success = true;
|
|
183
|
+
// Add timeout message specifically if that was the cause
|
|
184
|
+
if (output.includes("TIMEOUT: CODESYS process aborted")) {
|
|
185
|
+
// Message already added by the abort handler
|
|
186
|
+
}
|
|
187
|
+
else if (exitCode === null && !output.includes("Spawn Error:")) {
|
|
188
|
+
// If exit code is null but not due to spawn error or handled timeout, add generic killed message
|
|
189
|
+
output += `\nProcess may have been killed externally or timed out without proper exit code reporting.`;
|
|
149
190
|
}
|
|
150
191
|
}
|
|
151
192
|
}
|
|
152
193
|
}
|
|
153
194
|
catch (error) {
|
|
154
|
-
process.stderr.write(`Error executing CODESYS script: ${error}\n`);
|
|
155
|
-
output += `\nSCRIPT_ERROR: Failed to
|
|
195
|
+
process.stderr.write(`Error executing CODESYS script: ${error}\n`);
|
|
196
|
+
output += `\nSCRIPT_ERROR: Failed to set up script execution process. ${error.message}`;
|
|
156
197
|
success = false;
|
|
157
198
|
}
|
|
158
199
|
finally {
|
|
159
200
|
// Clean up the temporary file
|
|
160
201
|
try {
|
|
161
202
|
yield (0, promises_1.unlink)(tempFilePath);
|
|
162
|
-
process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`);
|
|
203
|
+
process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`);
|
|
163
204
|
}
|
|
164
205
|
catch (cleanupError) {
|
|
165
|
-
process.stderr.write(`Failed to delete temporary script file ${tempFilePath}: ${cleanupError}\n`);
|
|
166
|
-
|
|
167
|
-
if (success) { // Only add cleanup error if the main process didn't already fail
|
|
206
|
+
process.stderr.write(`Failed to delete temporary script file ${tempFilePath}: ${cleanupError}\n`);
|
|
207
|
+
if (success) {
|
|
168
208
|
output += `\nWARNING: Failed to delete temporary script file ${tempFilePath}. ${cleanupError.message}`;
|
|
169
209
|
}
|
|
170
210
|
}
|
|
171
211
|
}
|
|
172
|
-
process.stderr.write(`Final script output received:\n---\n${output}\n---\n`);
|
|
173
|
-
return { success, output };
|
|
212
|
+
process.stderr.write(`Final script output received (Success: ${success}):\n---\n${output}\n---\n`);
|
|
213
|
+
return { success, output };
|
|
174
214
|
});
|
|
175
215
|
}
|