Skip to content

Commit c89ac0f

Browse files
committed
add commandline executor block
1 parent 1398154 commit c89ac0f

File tree

7 files changed

+288
-0
lines changed

7 files changed

+288
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { spawn } from "child_process";
2+
import { NextRequest, NextResponse } from "next/server";
3+
import type { CommandInput, CommandOutput } from "@/tools/command/types";
4+
5+
export async function POST(request: NextRequest) {
6+
try {
7+
const params: CommandInput = await request.json();
8+
9+
// Validate input
10+
if (!params.command) {
11+
return NextResponse.json(
12+
{ error: "Command is required" },
13+
{ status: 400 },
14+
);
15+
}
16+
17+
// Set default values
18+
const workingDirectory = params.workingDirectory || process.cwd();
19+
const timeout = params.timeout || 30000;
20+
const shell = params.shell || "/bin/bash";
21+
22+
// Execute command
23+
const startTime = Date.now();
24+
const result = await executeCommand(
25+
params.command,
26+
workingDirectory,
27+
timeout,
28+
shell,
29+
);
30+
const duration = Date.now() - startTime;
31+
32+
const output: CommandOutput = {
33+
stdout: result.stdout,
34+
stderr: result.stderr,
35+
exitCode: result.exitCode,
36+
duration,
37+
command: params.command,
38+
workingDirectory,
39+
timedOut: result.timedOut,
40+
};
41+
42+
return NextResponse.json(output);
43+
} catch (error) {
44+
console.error("Command execution error:", error);
45+
return NextResponse.json(
46+
{
47+
error: error instanceof Error ? error.message : "Unknown error occurred",
48+
},
49+
{ status: 500 },
50+
);
51+
}
52+
}
53+
54+
interface ExecutionResult {
55+
stdout: string;
56+
stderr: string;
57+
exitCode: number;
58+
timedOut: boolean;
59+
}
60+
61+
function executeCommand(
62+
command: string,
63+
workingDirectory: string,
64+
timeout: number,
65+
shell: string,
66+
): Promise<ExecutionResult> {
67+
return new Promise((resolve) => {
68+
let stdout = "";
69+
let stderr = "";
70+
let timedOut = false;
71+
72+
// Merge environment variables
73+
const env = {
74+
...process.env,
75+
};
76+
77+
// Spawn the process
78+
const proc = spawn(shell, ["-c", command], {
79+
cwd: workingDirectory,
80+
env,
81+
timeout,
82+
});
83+
84+
// Set up timeout
85+
const timeoutId = setTimeout(() => {
86+
timedOut = true;
87+
proc.kill("SIGTERM");
88+
89+
// Force kill after 5 seconds if still running
90+
setTimeout(() => {
91+
if (!proc.killed) {
92+
proc.kill("SIGKILL");
93+
}
94+
}, 5000);
95+
}, timeout);
96+
97+
// Capture stdout
98+
proc.stdout?.on("data", (data: Buffer) => {
99+
stdout += data.toString();
100+
});
101+
102+
// Capture stderr
103+
proc.stderr?.on("data", (data: Buffer) => {
104+
stderr += data.toString();
105+
});
106+
107+
// Handle process completion
108+
proc.on("close", (code: number | null) => {
109+
clearTimeout(timeoutId);
110+
resolve({
111+
stdout,
112+
stderr,
113+
exitCode: code ?? -1,
114+
timedOut,
115+
});
116+
});
117+
118+
// Handle process errors
119+
proc.on("error", (error: Error) => {
120+
clearTimeout(timeoutId);
121+
resolve({
122+
stdout,
123+
stderr: stderr + `\nProcess error: ${error.message}`,
124+
exitCode: -1,
125+
timedOut,
126+
});
127+
});
128+
});
129+
}

apps/sim/blocks/blocks/command.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { Terminal } from "lucide-react";
2+
import type { BlockConfig } from "@/blocks/types";
3+
4+
export const commandBlock: BlockConfig = {
5+
type: "command",
6+
name: "Command",
7+
description: "Execute bash commands in a specified working directory with optional timeout and shell configuration.",
8+
category: "tools",
9+
bgColor: "#10B981",
10+
icon: Terminal,
11+
subBlocks: [
12+
{
13+
id: "command",
14+
title: "Command",
15+
type: "long-input",
16+
placeholder: 'echo "Hello World"',
17+
required: true,
18+
},
19+
{
20+
id: "workingDirectory",
21+
title: "Working Directory",
22+
type: "short-input",
23+
placeholder: "/path/to/directory (optional)",
24+
required: false,
25+
},
26+
{
27+
id: "timeout",
28+
title: "Timeout (ms)",
29+
type: "short-input",
30+
placeholder: "30000",
31+
value: () => "30000",
32+
required: false,
33+
},
34+
{
35+
id: "shell",
36+
title: "Shell",
37+
type: "short-input",
38+
placeholder: "/bin/bash",
39+
value: () => "/bin/bash",
40+
required: false,
41+
},
42+
],
43+
tools: {
44+
access: ["command_exec"],
45+
config: {
46+
tool: () => "command_exec",
47+
params: (params: Record<string, any>) => {
48+
const transformed: Record<string, any> = {
49+
command: params.command,
50+
};
51+
52+
if (params.workingDirectory) {
53+
transformed.workingDirectory = params.workingDirectory;
54+
}
55+
56+
if (params.timeout) {
57+
const timeoutNum = Number.parseInt(params.timeout as string, 10);
58+
if (!Number.isNaN(timeoutNum)) {
59+
transformed.timeout = timeoutNum;
60+
}
61+
}
62+
63+
if (params.shell) {
64+
transformed.shell = params.shell;
65+
}
66+
67+
return transformed;
68+
},
69+
},
70+
},
71+
inputs: {
72+
command: { type: "string", description: "The bash command to execute" },
73+
workingDirectory: { type: "string", description: "Directory where the command will be executed" },
74+
timeout: { type: "number", description: "Maximum execution time in milliseconds" },
75+
shell: { type: "string", description: "Shell to use for execution" },
76+
},
77+
outputs: {
78+
stdout: { type: "string", description: "Standard output from the command" },
79+
stderr: { type: "string", description: "Standard error from the command" },
80+
exitCode: { type: "number", description: "Command exit code (0 = success)" },
81+
duration: { type: "number", description: "Execution time in milliseconds" },
82+
command: { type: "string", description: "The executed command" },
83+
workingDirectory: { type: "string", description: "The directory where command was executed" },
84+
timedOut: { type: "boolean", description: "Whether the command exceeded the timeout" },
85+
},
86+
};

apps/sim/blocks/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { CalendlyBlock } from '@/blocks/blocks/calendly'
1313
import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger'
1414
import { CirclebackBlock } from '@/blocks/blocks/circleback'
1515
import { ClayBlock } from '@/blocks/blocks/clay'
16+
import { commandBlock } from '@/blocks/blocks/command'
1617
import { ConditionBlock } from '@/blocks/blocks/condition'
1718
import { ConfluenceBlock } from '@/blocks/blocks/confluence'
1819
import { CursorBlock } from '@/blocks/blocks/cursor'
@@ -162,6 +163,7 @@ export const registry: Record<string, BlockConfig> = {
162163
chat_trigger: ChatTriggerBlock,
163164
circleback: CirclebackBlock,
164165
clay: ClayBlock,
166+
command: commandBlock,
165167
condition: ConditionBlock,
166168
confluence: ConfluenceBlock,
167169
cursor: CursorBlock,

apps/sim/tools/command/chat.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { CommandInput, CommandOutput } from '@/tools/command/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
export const commandExecTool: ToolConfig<CommandInput, CommandOutput> = {
5+
id: 'command_exec',
6+
name: 'Command',
7+
description: 'Execute bash commands with custom environment variables',
8+
version: '1.0.0',
9+
10+
params: {
11+
command: {
12+
type: 'string',
13+
required: true,
14+
visibility: 'user-or-llm',
15+
description: 'The bash command to execute',
16+
},
17+
workingDirectory: {
18+
type: 'string',
19+
required: false,
20+
visibility: 'user-only',
21+
description: 'Directory where the command will be executed',
22+
},
23+
timeout: {
24+
type: 'number',
25+
required: false,
26+
visibility: 'user-only',
27+
description: 'Maximum execution time in milliseconds',
28+
},
29+
shell: {
30+
type: 'string',
31+
required: false,
32+
visibility: 'user-only',
33+
description: 'Shell to use for execution',
34+
},
35+
},
36+
37+
request: {
38+
url: '/api/tools/command/exec',
39+
method: 'POST',
40+
headers: () => ({
41+
'Content-Type': 'application/json',
42+
}),
43+
body: (params) => ({
44+
command: params.command,
45+
workingDirectory: params.workingDirectory,
46+
timeout: params.timeout || 30000,
47+
shell: params.shell || '/bin/bash',
48+
}),
49+
},
50+
};

apps/sim/tools/command/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './chat'
2+
export * from './types'

apps/sim/tools/command/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export interface CommandInput {
2+
command: string; // The bash command to execute
3+
workingDirectory?: string; // Optional working directory (defaults to workspace root)
4+
timeout?: number; // Optional timeout in milliseconds (default: 30000)
5+
shell?: string; // Optional shell to use (default: /bin/bash)
6+
}
7+
8+
export interface CommandOutput {
9+
stdout: string; // Standard output from the command
10+
stderr: string; // Standard error from the command
11+
exitCode: number; // Exit code of the command
12+
duration: number; // Execution duration in milliseconds
13+
command: string; // The executed command
14+
workingDirectory: string; // The directory where command was executed
15+
timedOut: boolean; // Whether the command timed out
16+
}

apps/sim/tools/registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,7 @@ import {
11671167
sshWriteFileContentTool,
11681168
} from '@/tools/ssh'
11691169
import { stagehandAgentTool, stagehandExtractTool } from '@/tools/stagehand'
1170+
import { commandExecTool } from '@/tools/command'
11701171
import {
11711172
stripeCancelPaymentIntentTool,
11721173
stripeCancelSubscriptionTool,
@@ -2020,6 +2021,8 @@ export const tools: Record<string, ToolConfig> = {
20202021
thinking_tool: thinkingTool,
20212022
stagehand_extract: stagehandExtractTool,
20222023
stagehand_agent: stagehandAgentTool,
2024+
opencode_chat: opencodeChatTool,
2025+
command_exec: commandExecTool,
20232026
mem0_add_memories: mem0AddMemoriesTool,
20242027
mem0_search_memories: mem0SearchMemoriesTool,
20252028
mem0_get_memories: mem0GetMemoriesTool,

0 commit comments

Comments
 (0)