Skip to content

Commit 660fa34

Browse files
committed
add default run_terminal_command implementation
1 parent abd1cb0 commit 660fa34

File tree

2 files changed

+125
-15
lines changed

2 files changed

+125
-15
lines changed

sdk/src/client.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import os from 'os'
44
import { CODEBUFF_BINARY } from './constants'
55
import { changeFile } from './tools/change-file'
66
import { getFiles } from './tools/read-files'
7+
import { runTerminalCommand } from './tools/run-terminal-command'
78
import { WebSocketHandler } from './websocket-client'
89
import {
910
PromptResponseSchema,
@@ -215,7 +216,9 @@ export class CodebuffClient {
215216
return getFiles(filePath, this.cwd)
216217
}
217218

218-
private async handleToolCall(action: ServerAction<'tool-call-request'>) {
219+
private async handleToolCall(
220+
action: ServerAction<'tool-call-request'>,
221+
): ReturnType<WebSocketHandler['handleToolCall']> {
219222
const toolName = action.toolName
220223
const input = action.input
221224
let result: string
@@ -234,32 +237,39 @@ export class CodebuffClient {
234237
const r = changeFile(input, this.cwd)
235238
result = r.toolResultMessage
236239
} else if (toolName === 'run_terminal_command') {
237-
throw new Error(
238-
'run_terminal_command not implemented in SDK yet; please provide an override.',
239-
)
240+
const r = await runTerminalCommand({
241+
...input,
242+
cwd: input.cwd ?? this.cwd,
243+
} as Parameters<typeof runTerminalCommand>[0])
244+
result = r.output
240245
} else {
241246
throw new Error(
242247
`Tool not implemented in SDK. Please provide an override or modify your agent to not use this tool: ${toolName}`,
243248
)
244249
}
245250
} catch (error) {
246251
return {
247-
type: 'tool-call-response',
248-
requestId: action.requestId,
249252
success: false,
250-
result:
251-
error && typeof error === 'object' && 'message' in error
252-
? error.message
253-
: typeof error === 'string'
254-
? error
255-
: 'Unknown error',
253+
output: {
254+
type: 'text',
255+
value:
256+
error &&
257+
typeof error === 'object' &&
258+
'message' in error &&
259+
typeof error.message === 'string'
260+
? error.message
261+
: typeof error === 'string'
262+
? error
263+
: 'Unknown error',
264+
},
256265
}
257266
}
258267
return {
259-
type: 'tool-call-response',
260-
requestId: action.requestId,
261268
success: true,
262-
result,
269+
output: {
270+
type: 'text',
271+
value: result,
272+
},
263273
}
264274
}
265275
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { spawn } from 'child_process'
2+
import * as os from 'os'
3+
import * as path from 'path'
4+
5+
import { buildArray } from '../../../common/src/util/array'
6+
7+
export function runTerminalCommand({
8+
command,
9+
process_type,
10+
cwd,
11+
timeout_seconds,
12+
}: {
13+
command: string
14+
process_type: 'SYNC' | 'BACKGROUND'
15+
cwd: string
16+
timeout_seconds: number
17+
}): Promise<{ output: string }> {
18+
if (process_type === 'BACKGROUND') {
19+
throw new Error('BACKGROUND process_type not implemented')
20+
}
21+
22+
return new Promise((resolve, reject) => {
23+
const isWindows = os.platform() === 'win32'
24+
const shell = isWindows ? 'cmd.exe' : 'bash'
25+
const shellArgs = isWindows ? ['/c'] : ['-c']
26+
27+
// Resolve cwd to absolute path
28+
const resolvedCwd = path.resolve(cwd)
29+
30+
const childProcess = spawn(shell, [...shellArgs, command], {
31+
cwd: resolvedCwd,
32+
env: {
33+
...process.env,
34+
FORCE_COLOR: '1',
35+
CLICOLOR: '1',
36+
CLICOLOR_FORCE: '1',
37+
},
38+
stdio: 'pipe',
39+
})
40+
41+
let stdout = ''
42+
let stderr = ''
43+
let timer: NodeJS.Timeout | null = null
44+
let processFinished = false
45+
46+
// Set up timeout if timeout_seconds >= 0 (infinite timeout when < 0)
47+
if (timeout_seconds >= 0) {
48+
timer = setTimeout(() => {
49+
if (!processFinished) {
50+
processFinished = true
51+
childProcess.kill('SIGTERM')
52+
reject(
53+
new Error(`Command timed out after ${timeout_seconds} seconds`),
54+
)
55+
}
56+
}, timeout_seconds * 1000)
57+
}
58+
59+
// Collect stdout
60+
childProcess.stdout.on('data', (data: Buffer) => {
61+
stdout += data.toString()
62+
})
63+
64+
// Collect stderr
65+
childProcess.stderr.on('data', (data: Buffer) => {
66+
stderr += data.toString()
67+
})
68+
69+
// Handle process completion
70+
childProcess.on('close', (exitCode) => {
71+
if (processFinished) return
72+
processFinished = true
73+
74+
if (timer) {
75+
clearTimeout(timer)
76+
}
77+
78+
// Include stderr in stdout for compatibility with existing behavior
79+
const combinedOutput = buildArray([
80+
`\`\`\`stdout\n${stdout}\`\`\``,
81+
stderr && `\`\`\`stderr\n${stderr}\`\`\``,
82+
exitCode !== null && `\`\`\`exit_code\n${exitCode}\`\`\``,
83+
]).join('\n\n')
84+
85+
resolve({ output: combinedOutput })
86+
})
87+
88+
// Handle spawn errors
89+
childProcess.on('error', (error) => {
90+
if (processFinished) return
91+
processFinished = true
92+
93+
if (timer) {
94+
clearTimeout(timer)
95+
}
96+
97+
reject(new Error(`Failed to spawn command: ${error.message}`))
98+
})
99+
})
100+
}

0 commit comments

Comments
 (0)