From 5ab14ffb9e6ff5b11cfa99736f6e217bc5c7f26c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 23 Sep 2025 14:17:46 +0200 Subject: [PATCH] Allow code execution to use the console, returning the full log --- mcp-server/src/tools/ExecuteCodeTool.ts | 5 +- .../task-handlers/ExecuteCodeTaskHandler.ts | 172 +++++++++++++++++- 2 files changed, 171 insertions(+), 6 deletions(-) diff --git a/mcp-server/src/tools/ExecuteCodeTool.ts b/mcp-server/src/tools/ExecuteCodeTool.ts index 34e6d59..b791de3 100644 --- a/mcp-server/src/tools/ExecuteCodeTool.ts +++ b/mcp-server/src/tools/ExecuteCodeTool.ts @@ -45,8 +45,9 @@ export class ExecuteCodeTool extends Tool { "data can be stored, simply by adding a new attribute; stored attributes can be referenced in future calls " + "to this tool, so any intermediate results that could come in handy later should be stored in `storage` " + "instead of just a fleeting variable).\n" + - "The tool call returns the value of the concluding return statement, if any.\n" + - "Note that using console.log() in your code makes no sense as you will not see the output.\n" + + "The tool call returns the value of the concluding `return` statement, if any.\n" + + "Any output that you generate via the `console` object will be returned to you; so you may use this" + + "to track what your code is doing, but you should only do so only if there is an actual need!.\n" + "In general, try a simple approach first, and only if it fails, try more complex code that involves " + "handling different cases (in particular error cases)." ); diff --git a/penpot-plugin/src/task-handlers/ExecuteCodeTaskHandler.ts b/penpot-plugin/src/task-handlers/ExecuteCodeTaskHandler.ts index 94a79cb..f7c9eeb 100644 --- a/penpot-plugin/src/task-handlers/ExecuteCodeTaskHandler.ts +++ b/penpot-plugin/src/task-handlers/ExecuteCodeTaskHandler.ts @@ -1,27 +1,184 @@ import { Task, TaskHandler } from "../TaskHandler"; import { ExecuteCodeTaskParams } from "../../../common/src"; +/** + * Console implementation that captures all log output for code execution. + * + * Provides the same interface as the native console object but appends + * all output to an internal log string that can be retrieved. + */ +class ExecuteCodeTaskConsole { + /** + * Accumulated log output from all console method calls. + */ + private logOutput: string = ""; + + /** + * Resets the accumulated log output to empty string. + * Should be called before each code execution to start with clean logs. + */ + resetLog(): void { + this.logOutput = ""; + } + + /** + * Gets the accumulated log output from all console method calls. + * @returns The complete log output as a string + */ + getLog(): string { + return this.logOutput; + } + + /** + * Appends a formatted message to the log output. + * @param level - Log level prefix (e.g., "LOG", "WARN", "ERROR") + * @param args - Arguments to log, will be stringified and joined + */ + private appendToLog(level: string, ...args: any[]): void { + const message = args + .map((arg) => (typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg))) + .join(" "); + this.logOutput += `[${level}] ${message}\n`; + } + + /** + * Logs a message to the captured output. + */ + log(...args: any[]): void { + this.appendToLog("LOG", ...args); + } + + /** + * Logs a warning message to the captured output. + */ + warn(...args: any[]): void { + this.appendToLog("WARN", ...args); + } + + /** + * Logs an error message to the captured output. + */ + error(...args: any[]): void { + this.appendToLog("ERROR", ...args); + } + + /** + * Logs an informational message to the captured output. + */ + info(...args: any[]): void { + this.appendToLog("INFO", ...args); + } + + /** + * Logs a debug message to the captured output. + */ + debug(...args: any[]): void { + this.appendToLog("DEBUG", ...args); + } + + /** + * Logs a message with trace information to the captured output. + */ + trace(...args: any[]): void { + this.appendToLog("TRACE", ...args); + } + + /** + * Logs a table to the captured output (simplified as JSON). + */ + table(data: any): void { + this.appendToLog("TABLE", data); + } + + /** + * Starts a timer (simplified implementation that just logs). + */ + time(label?: string): void { + this.appendToLog("TIME", `Timer started: ${label || "default"}`); + } + + /** + * Ends a timer (simplified implementation that just logs). + */ + timeEnd(label?: string): void { + this.appendToLog("TIME_END", `Timer ended: ${label || "default"}`); + } + + /** + * Logs messages in a group (simplified to just log the label). + */ + group(label?: string): void { + this.appendToLog("GROUP", label || ""); + } + + /** + * Logs messages in a collapsed group (simplified to just log the label). + */ + groupCollapsed(label?: string): void { + this.appendToLog("GROUP_COLLAPSED", label || ""); + } + + /** + * Ends the current group (simplified implementation). + */ + groupEnd(): void { + this.appendToLog("GROUP_END", ""); + } + + /** + * Clears the console (no-op in this implementation since we want to capture logs). + */ + clear(): void { + // intentionally empty - we don't want to clear captured logs + } + + /** + * Counts occurrences of calls with the same label (simplified implementation). + */ + count(label?: string): void { + this.appendToLog("COUNT", label || "default"); + } + + /** + * Resets the count for a label (simplified implementation). + */ + countReset(label?: string): void { + this.appendToLog("COUNT_RESET", label || "default"); + } + + /** + * Logs an assertion (simplified to just log if condition is false). + */ + assert(condition: boolean, ...args: any[]): void { + if (!condition) { + this.appendToLog("ASSERT", ...args); + } + } +} + /** * Task handler for executing JavaScript code in the plugin context. * - * Maintains a persistent context object that preserves state between code executions. + * Maintains a persistent context object that preserves state between code executions + * and captures all console output during execution. */ export class ExecuteCodeTaskHandler extends TaskHandler { readonly taskType = "executeCode"; /** * Persistent context object that maintains state between code executions. - * Contains the penpot API and any variables defined in executed code. + * Contains the penpot API, storage object, and custom console implementation. */ private readonly context: any; constructor() { super(); - // initialize context, making penpot object available + // initialize context, making penpot object available with custom console this.context = { penpot: penpot, storage: {}, + console: new ExecuteCodeTaskConsole(), }; } @@ -31,6 +188,8 @@ export class ExecuteCodeTaskHandler extends TaskHandler { return; } + this.context.console.resetLog(); + const context = this.context; const code = task.params.code; @@ -39,6 +198,11 @@ export class ExecuteCodeTaskHandler extends TaskHandler { })(context); console.log("Code execution result:", result); - task.sendSuccess(result); + + // return both result and captured log + task.sendSuccess({ + result: result, + log: this.context.console.getLog(), + }); } }