From c89ac0fdc689e40f3e90108e3dd5b475bb1819ec Mon Sep 17 00:00:00 2001 From: JG <47306065+jgranesa@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:19:04 +0100 Subject: [PATCH 1/7] add commandline executor block --- apps/sim/app/api/tools/command/exec/route.ts | 129 +++++++++++++++++++ apps/sim/blocks/blocks/command.ts | 86 +++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/tools/command/chat.ts | 50 +++++++ apps/sim/tools/command/index.ts | 2 + apps/sim/tools/command/types.ts | 16 +++ apps/sim/tools/registry.ts | 3 + 7 files changed, 288 insertions(+) create mode 100644 apps/sim/app/api/tools/command/exec/route.ts create mode 100644 apps/sim/blocks/blocks/command.ts create mode 100644 apps/sim/tools/command/chat.ts create mode 100644 apps/sim/tools/command/index.ts create mode 100644 apps/sim/tools/command/types.ts diff --git a/apps/sim/app/api/tools/command/exec/route.ts b/apps/sim/app/api/tools/command/exec/route.ts new file mode 100644 index 0000000000..2f063ef6f4 --- /dev/null +++ b/apps/sim/app/api/tools/command/exec/route.ts @@ -0,0 +1,129 @@ +import { spawn } from "child_process"; +import { NextRequest, NextResponse } from "next/server"; +import type { CommandInput, CommandOutput } from "@/tools/command/types"; + +export async function POST(request: NextRequest) { + try { + const params: CommandInput = await request.json(); + + // Validate input + if (!params.command) { + return NextResponse.json( + { error: "Command is required" }, + { status: 400 }, + ); + } + + // Set default values + const workingDirectory = params.workingDirectory || process.cwd(); + const timeout = params.timeout || 30000; + const shell = params.shell || "/bin/bash"; + + // Execute command + const startTime = Date.now(); + const result = await executeCommand( + params.command, + workingDirectory, + timeout, + shell, + ); + const duration = Date.now() - startTime; + + const output: CommandOutput = { + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + duration, + command: params.command, + workingDirectory, + timedOut: result.timedOut, + }; + + return NextResponse.json(output); + } catch (error) { + console.error("Command execution error:", error); + return NextResponse.json( + { + error: error instanceof Error ? error.message : "Unknown error occurred", + }, + { status: 500 }, + ); + } +} + +interface ExecutionResult { + stdout: string; + stderr: string; + exitCode: number; + timedOut: boolean; +} + +function executeCommand( + command: string, + workingDirectory: string, + timeout: number, + shell: string, +): Promise { + return new Promise((resolve) => { + let stdout = ""; + let stderr = ""; + let timedOut = false; + + // Merge environment variables + const env = { + ...process.env, + }; + + // Spawn the process + const proc = spawn(shell, ["-c", command], { + cwd: workingDirectory, + env, + timeout, + }); + + // Set up timeout + const timeoutId = setTimeout(() => { + timedOut = true; + proc.kill("SIGTERM"); + + // Force kill after 5 seconds if still running + setTimeout(() => { + if (!proc.killed) { + proc.kill("SIGKILL"); + } + }, 5000); + }, timeout); + + // Capture stdout + proc.stdout?.on("data", (data: Buffer) => { + stdout += data.toString(); + }); + + // Capture stderr + proc.stderr?.on("data", (data: Buffer) => { + stderr += data.toString(); + }); + + // Handle process completion + proc.on("close", (code: number | null) => { + clearTimeout(timeoutId); + resolve({ + stdout, + stderr, + exitCode: code ?? -1, + timedOut, + }); + }); + + // Handle process errors + proc.on("error", (error: Error) => { + clearTimeout(timeoutId); + resolve({ + stdout, + stderr: stderr + `\nProcess error: ${error.message}`, + exitCode: -1, + timedOut, + }); + }); + }); +} diff --git a/apps/sim/blocks/blocks/command.ts b/apps/sim/blocks/blocks/command.ts new file mode 100644 index 0000000000..92bb26c188 --- /dev/null +++ b/apps/sim/blocks/blocks/command.ts @@ -0,0 +1,86 @@ +import { Terminal } from "lucide-react"; +import type { BlockConfig } from "@/blocks/types"; + +export const commandBlock: BlockConfig = { + type: "command", + name: "Command", + description: "Execute bash commands in a specified working directory with optional timeout and shell configuration.", + category: "tools", + bgColor: "#10B981", + icon: Terminal, + subBlocks: [ + { + id: "command", + title: "Command", + type: "long-input", + placeholder: 'echo "Hello World"', + required: true, + }, + { + id: "workingDirectory", + title: "Working Directory", + type: "short-input", + placeholder: "/path/to/directory (optional)", + required: false, + }, + { + id: "timeout", + title: "Timeout (ms)", + type: "short-input", + placeholder: "30000", + value: () => "30000", + required: false, + }, + { + id: "shell", + title: "Shell", + type: "short-input", + placeholder: "/bin/bash", + value: () => "/bin/bash", + required: false, + }, + ], + tools: { + access: ["command_exec"], + config: { + tool: () => "command_exec", + params: (params: Record) => { + const transformed: Record = { + command: params.command, + }; + + if (params.workingDirectory) { + transformed.workingDirectory = params.workingDirectory; + } + + if (params.timeout) { + const timeoutNum = Number.parseInt(params.timeout as string, 10); + if (!Number.isNaN(timeoutNum)) { + transformed.timeout = timeoutNum; + } + } + + if (params.shell) { + transformed.shell = params.shell; + } + + return transformed; + }, + }, + }, + inputs: { + command: { type: "string", description: "The bash command to execute" }, + workingDirectory: { type: "string", description: "Directory where the command will be executed" }, + timeout: { type: "number", description: "Maximum execution time in milliseconds" }, + shell: { type: "string", description: "Shell to use for execution" }, + }, + outputs: { + stdout: { type: "string", description: "Standard output from the command" }, + stderr: { type: "string", description: "Standard error from the command" }, + exitCode: { type: "number", description: "Command exit code (0 = success)" }, + duration: { type: "number", description: "Execution time in milliseconds" }, + command: { type: "string", description: "The executed command" }, + workingDirectory: { type: "string", description: "The directory where command was executed" }, + timedOut: { type: "boolean", description: "Whether the command exceeded the timeout" }, + }, +}; diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 8a4d75121f..f0d1eb30e5 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -13,6 +13,7 @@ import { CalendlyBlock } from '@/blocks/blocks/calendly' import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger' import { CirclebackBlock } from '@/blocks/blocks/circleback' import { ClayBlock } from '@/blocks/blocks/clay' +import { commandBlock } from '@/blocks/blocks/command' import { ConditionBlock } from '@/blocks/blocks/condition' import { ConfluenceBlock } from '@/blocks/blocks/confluence' import { CursorBlock } from '@/blocks/blocks/cursor' @@ -162,6 +163,7 @@ export const registry: Record = { chat_trigger: ChatTriggerBlock, circleback: CirclebackBlock, clay: ClayBlock, + command: commandBlock, condition: ConditionBlock, confluence: ConfluenceBlock, cursor: CursorBlock, diff --git a/apps/sim/tools/command/chat.ts b/apps/sim/tools/command/chat.ts new file mode 100644 index 0000000000..e6d300d0e3 --- /dev/null +++ b/apps/sim/tools/command/chat.ts @@ -0,0 +1,50 @@ +import type { CommandInput, CommandOutput } from '@/tools/command/types' +import type { ToolConfig } from '@/tools/types' + +export const commandExecTool: ToolConfig = { + id: 'command_exec', + name: 'Command', + description: 'Execute bash commands with custom environment variables', + version: '1.0.0', + + params: { + command: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The bash command to execute', + }, + workingDirectory: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Directory where the command will be executed', + }, + timeout: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum execution time in milliseconds', + }, + shell: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Shell to use for execution', + }, + }, + + request: { + url: '/api/tools/command/exec', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + command: params.command, + workingDirectory: params.workingDirectory, + timeout: params.timeout || 30000, + shell: params.shell || '/bin/bash', + }), + }, +}; diff --git a/apps/sim/tools/command/index.ts b/apps/sim/tools/command/index.ts new file mode 100644 index 0000000000..20e4d42ac5 --- /dev/null +++ b/apps/sim/tools/command/index.ts @@ -0,0 +1,2 @@ +export * from './chat' +export * from './types' diff --git a/apps/sim/tools/command/types.ts b/apps/sim/tools/command/types.ts new file mode 100644 index 0000000000..2e356b95fe --- /dev/null +++ b/apps/sim/tools/command/types.ts @@ -0,0 +1,16 @@ +export interface CommandInput { + command: string; // The bash command to execute + workingDirectory?: string; // Optional working directory (defaults to workspace root) + timeout?: number; // Optional timeout in milliseconds (default: 30000) + shell?: string; // Optional shell to use (default: /bin/bash) +} + +export interface CommandOutput { + stdout: string; // Standard output from the command + stderr: string; // Standard error from the command + exitCode: number; // Exit code of the command + duration: number; // Execution duration in milliseconds + command: string; // The executed command + workingDirectory: string; // The directory where command was executed + timedOut: boolean; // Whether the command timed out +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 6d8cb9ec2a..2689a0731a 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1167,6 +1167,7 @@ import { sshWriteFileContentTool, } from '@/tools/ssh' import { stagehandAgentTool, stagehandExtractTool } from '@/tools/stagehand' +import { commandExecTool } from '@/tools/command' import { stripeCancelPaymentIntentTool, stripeCancelSubscriptionTool, @@ -2020,6 +2021,8 @@ export const tools: Record = { thinking_tool: thinkingTool, stagehand_extract: stagehandExtractTool, stagehand_agent: stagehandAgentTool, + opencode_chat: opencodeChatTool, + command_exec: commandExecTool, mem0_add_memories: mem0AddMemoriesTool, mem0_search_memories: mem0SearchMemoriesTool, mem0_get_memories: mem0GetMemoriesTool, From e7553d1a1d8c716fd0ccd8760932652c3964f0f1 Mon Sep 17 00:00:00 2001 From: Jaume <47306065+jgranesa@users.noreply.github.com> Date: Fri, 9 Jan 2026 14:03:36 +0100 Subject: [PATCH 2/7] Update apps/sim/app/api/tools/command/exec/route.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- apps/sim/app/api/tools/command/exec/route.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/command/exec/route.ts b/apps/sim/app/api/tools/command/exec/route.ts index 2f063ef6f4..757db6e904 100644 --- a/apps/sim/app/api/tools/command/exec/route.ts +++ b/apps/sim/app/api/tools/command/exec/route.ts @@ -41,7 +41,12 @@ export async function POST(request: NextRequest) { return NextResponse.json(output); } catch (error) { - console.error("Command execution error:", error); +import { createLogger } from '@sim/logger' + +const logger = createLogger('CommandExecAPI') + +// Then replace console.error with: + logger.error('Command execution error:', { error }) return NextResponse.json( { error: error instanceof Error ? error.message : "Unknown error occurred", From d945e659843c0713a8efc5a6018599ccd7d2a762 Mon Sep 17 00:00:00 2001 From: Jaume <47306065+jgranesa@users.noreply.github.com> Date: Fri, 9 Jan 2026 14:09:18 +0100 Subject: [PATCH 3/7] Update apps/sim/app/api/tools/command/exec/route.ts add authentication check using session Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- apps/sim/app/api/tools/command/exec/route.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/api/tools/command/exec/route.ts b/apps/sim/app/api/tools/command/exec/route.ts index 757db6e904..e9ec305f00 100644 --- a/apps/sim/app/api/tools/command/exec/route.ts +++ b/apps/sim/app/api/tools/command/exec/route.ts @@ -2,18 +2,20 @@ import { spawn } from "child_process"; import { NextRequest, NextResponse } from "next/server"; import type { CommandInput, CommandOutput } from "@/tools/command/types"; +import { getSession } from '@/lib/auth' + export async function POST(request: NextRequest) { try { - const params: CommandInput = await request.json(); - - // Validate input - if (!params.command) { + const session = await getSession() + if (!session?.user?.id) { return NextResponse.json( - { error: "Command is required" }, - { status: 400 }, - ); + { error: 'Unauthorized' }, + { status: 401 }, + ) } + const params: CommandInput = await request.json() + // Set default values const workingDirectory = params.workingDirectory || process.cwd(); const timeout = params.timeout || 30000; From c385c9ff45738ec9024b391b161612a0cf364aeb Mon Sep 17 00:00:00 2001 From: Jaume <47306065+jgranesa@users.noreply.github.com> Date: Sat, 10 Jan 2026 11:20:19 +0100 Subject: [PATCH 4/7] Update apps/sim/app/api/tools/command/exec/route.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- apps/sim/app/api/tools/command/exec/route.ts | 40 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/api/tools/command/exec/route.ts b/apps/sim/app/api/tools/command/exec/route.ts index e9ec305f00..e54de42923 100644 --- a/apps/sim/app/api/tools/command/exec/route.ts +++ b/apps/sim/app/api/tools/command/exec/route.ts @@ -16,11 +16,43 @@ export async function POST(request: NextRequest) { const params: CommandInput = await request.json() - // Set default values - const workingDirectory = params.workingDirectory || process.cwd(); - const timeout = params.timeout || 30000; - const shell = params.shell || "/bin/bash"; +import { validatePathSegment } from '@/lib/core/security/input-validation' + // Validate input + if (!params.command) { + return NextResponse.json( + { error: "Command is required" }, + { status: 400 }, + ) + } + + // Validate workingDirectory if provided + if (params.workingDirectory) { + const validation = validatePathSegment(params.workingDirectory, { + paramName: 'workingDirectory', + allowDots: true // Allow relative paths like ../ + }) + if (!validation.isValid) { + return NextResponse.json( + { error: validation.error }, + { status: 400 }, + ) + } + } + + // Validate shell if provided - only allow safe shells + const allowedShells = ['/bin/bash', '/bin/sh', '/bin/zsh'] + if (params.shell && !allowedShells.includes(params.shell)) { + return NextResponse.json( + { error: 'Invalid shell. Allowed shells: ' + allowedShells.join(', ') }, + { status: 400 }, + ) + } + + // Set default values + const workingDirectory = params.workingDirectory || process.cwd() + const timeout = params.timeout || 30000 + const shell = params.shell || '/bin/bash' // Execute command const startTime = Date.now(); const result = await executeCommand( From adc798675662a1c91f77b6646bcc76ea2f846296 Mon Sep 17 00:00:00 2001 From: Jaume <47306065+jgranesa@users.noreply.github.com> Date: Sat, 10 Jan 2026 11:22:05 +0100 Subject: [PATCH 5/7] Update apps/sim/tools/command/chat.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- apps/sim/tools/command/chat.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/sim/tools/command/chat.ts b/apps/sim/tools/command/chat.ts index e6d300d0e3..83e80de8c9 100644 --- a/apps/sim/tools/command/chat.ts +++ b/apps/sim/tools/command/chat.ts @@ -47,4 +47,15 @@ export const commandExecTool: ToolConfig = { shell: params.shell || '/bin/bash', }), }, + }, + + outputs: { + stdout: { type: 'string', description: 'Standard output from the command' }, + stderr: { type: 'string', description: 'Standard error from the command' }, + exitCode: { type: 'number', description: 'Command exit code (0 = success)' }, + duration: { type: 'number', description: 'Execution time in milliseconds' }, + command: { type: 'string', description: 'The executed command' }, + workingDirectory: { type: 'string', description: 'The directory where command was executed' }, + timedOut: { type: 'boolean', description: 'Whether the command exceeded the timeout' }, + }, }; From 67b0831f515c45a2d176a1272c7417713f2cbaef Mon Sep 17 00:00:00 2001 From: JG <47306065+jgranesa@users.noreply.github.com> Date: Sat, 10 Jan 2026 11:24:52 +0100 Subject: [PATCH 6/7] feat use sim logger rather than console log --- apps/sim/app/api/tools/command/exec/route.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/api/tools/command/exec/route.ts b/apps/sim/app/api/tools/command/exec/route.ts index e54de42923..ff2697b881 100644 --- a/apps/sim/app/api/tools/command/exec/route.ts +++ b/apps/sim/app/api/tools/command/exec/route.ts @@ -1,9 +1,11 @@ +import { createLogger } from '@sim/logger' import { spawn } from "child_process"; import { NextRequest, NextResponse } from "next/server"; import type { CommandInput, CommandOutput } from "@/tools/command/types"; - import { getSession } from '@/lib/auth' +const logger = createLogger('CommandExecAPI') + export async function POST(request: NextRequest) { try { const session = await getSession() @@ -75,11 +77,6 @@ import { validatePathSegment } from '@/lib/core/security/input-validation' return NextResponse.json(output); } catch (error) { -import { createLogger } from '@sim/logger' - -const logger = createLogger('CommandExecAPI') - -// Then replace console.error with: logger.error('Command execution error:', { error }) return NextResponse.json( { From 0da00165f2e0d203130427f00ee5b4c65792903a Mon Sep 17 00:00:00 2001 From: JG <47306065+jgranesa@users.noreply.github.com> Date: Sat, 10 Jan 2026 11:34:13 +0100 Subject: [PATCH 7/7] chore(block): remove unused and unnecessary code --- apps/sim/tools/registry.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 2689a0731a..d94193908f 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2021,7 +2021,6 @@ export const tools: Record = { thinking_tool: thinkingTool, stagehand_extract: stagehandExtractTool, stagehand_agent: stagehandAgentTool, - opencode_chat: opencodeChatTool, command_exec: commandExecTool, mem0_add_memories: mem0AddMemoriesTool, mem0_search_memories: mem0SearchMemoriesTool,