Skip to content

Commit 231066e

Browse files
authored
Merge pull request #175 from drivecore/feature/167-shell-tools-stdin-stdout-visibility
feat: add showStdIn and showStdout options to shellMessage and shellS…
2 parents c3750e8 + cc192c5 commit 231066e

File tree

4 files changed

+180
-5
lines changed

4 files changed

+180
-5
lines changed

packages/agent/src/tools/system/shellMessage.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,72 @@ describe('shellMessageTool', () => {
214214
expect(checkResult.completed).toBe(true);
215215
expect(processStates.has(instanceId)).toBe(true);
216216
});
217+
218+
it('should respect showStdIn and showStdout parameters', async () => {
219+
// Start a process with default visibility settings
220+
const startResult = await shellStartTool.execute(
221+
{
222+
command: 'cat',
223+
description: 'Test with stdin/stdout visibility',
224+
timeout: 50, // Force async mode
225+
},
226+
toolContext,
227+
);
228+
229+
const instanceId = getInstanceId(startResult);
230+
231+
// Verify process state has default visibility settings
232+
const processState = processStates.get(instanceId);
233+
expect(processState?.showStdIn).toBe(false);
234+
expect(processState?.showStdout).toBe(false);
235+
236+
// Send input with explicit visibility settings
237+
await shellMessageTool.execute(
238+
{
239+
instanceId,
240+
stdin: 'test input',
241+
description: 'Test with explicit visibility settings',
242+
showStdIn: true,
243+
showStdout: true,
244+
},
245+
toolContext,
246+
);
247+
248+
// Verify process state still exists
249+
expect(processStates.has(instanceId)).toBe(true);
250+
});
251+
252+
it('should inherit visibility settings from process state', async () => {
253+
// Start a process with explicit visibility settings
254+
const startResult = await shellStartTool.execute(
255+
{
256+
command: 'cat',
257+
description: 'Test with inherited visibility settings',
258+
timeout: 50, // Force async mode
259+
showStdIn: true,
260+
showStdout: true,
261+
},
262+
toolContext,
263+
);
264+
265+
const instanceId = getInstanceId(startResult);
266+
267+
// Verify process state has the specified visibility settings
268+
const processState = processStates.get(instanceId);
269+
expect(processState?.showStdIn).toBe(true);
270+
expect(processState?.showStdout).toBe(true);
271+
272+
// Send input without specifying visibility settings
273+
await shellMessageTool.execute(
274+
{
275+
instanceId,
276+
stdin: 'test input',
277+
description: 'Test with inherited visibility settings',
278+
},
279+
toolContext,
280+
);
281+
282+
// Verify process state still exists
283+
expect(processStates.has(instanceId)).toBe(true);
284+
});
217285
});

packages/agent/src/tools/system/shellMessage.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ const parameterSchema = z.object({
5454
description: z
5555
.string()
5656
.describe('The reason for this shell interaction (max 80 chars)'),
57+
showStdIn: z
58+
.boolean()
59+
.optional()
60+
.describe(
61+
'Whether to show the input in the logs (default: false or value from shellStart)',
62+
),
63+
showStdout: z
64+
.boolean()
65+
.optional()
66+
.describe(
67+
'Whether to show output in the logs (default: false or value from shellStart)',
68+
),
5769
});
5870

5971
const returnSchema = z
@@ -82,7 +94,7 @@ export const shellMessageTool: Tool<Parameters, ReturnType> = {
8294
returnsJsonSchema: zodToJsonSchema(returnSchema),
8395

8496
execute: async (
85-
{ instanceId, stdin, signal },
97+
{ instanceId, stdin, signal, showStdIn, showStdout },
8698
{ logger },
8799
): Promise<ReturnType> => {
88100
logger.verbose(
@@ -115,6 +127,14 @@ export const shellMessageTool: Tool<Parameters, ReturnType> = {
115127
if (!processState.process.stdin?.writable) {
116128
throw new Error('Process stdin is not available');
117129
}
130+
131+
// Determine whether to show stdin (prefer explicit parameter, fall back to process state)
132+
const shouldShowStdIn =
133+
showStdIn !== undefined ? showStdIn : processState.showStdIn;
134+
if (shouldShowStdIn) {
135+
logger.info(`[${instanceId}] stdin: ${stdin}`);
136+
}
137+
118138
processState.process.stdin.write(`${stdin}\n`);
119139
}
120140

@@ -130,11 +150,22 @@ export const shellMessageTool: Tool<Parameters, ReturnType> = {
130150
processState.stderr = [];
131151

132152
logger.verbose('Interaction completed successfully');
153+
154+
// Determine whether to show stdout (prefer explicit parameter, fall back to process state)
155+
const shouldShowStdout =
156+
showStdout !== undefined ? showStdout : processState.showStdout;
157+
133158
if (stdout) {
134159
logger.verbose(`stdout: ${stdout.trim()}`);
160+
if (shouldShowStdout) {
161+
logger.info(`[${instanceId}] stdout: ${stdout.trim()}`);
162+
}
135163
}
136164
if (stderr) {
137165
logger.verbose(`stderr: ${stderr.trim()}`);
166+
if (shouldShowStdout) {
167+
logger.info(`[${instanceId}] stderr: ${stderr.trim()}`);
168+
}
138169
}
139170

140171
return {
@@ -168,8 +199,17 @@ export const shellMessageTool: Tool<Parameters, ReturnType> = {
168199

169200
logParameters: (input, { logger }) => {
170201
const processState = processStates.get(input.instanceId);
202+
const showStdIn =
203+
input.showStdIn !== undefined
204+
? input.showStdIn
205+
: processState?.showStdIn || false;
206+
const showStdout =
207+
input.showStdout !== undefined
208+
? input.showStdout
209+
: processState?.showStdout || false;
210+
171211
logger.info(
172-
`Interacting with shell command "${processState ? processState.command : '<unknown instanceId>'}", ${input.description}`,
212+
`Interacting with shell command "${processState ? processState.command : '<unknown instanceId>'}", ${input.description} (showStdIn: ${showStdIn}, showStdout: ${showStdout})`,
173213
);
174214
},
175215
logReturns: () => {},

packages/agent/src/tools/system/shellStart.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,37 @@ describe('shellStartTool', () => {
158158

159159
expect(result.mode).toBe('sync');
160160
});
161+
162+
it('should store showStdIn and showStdout settings in process state', async () => {
163+
const result = await shellStartTool.execute(
164+
{
165+
command: 'echo "test"',
166+
description: 'Test with stdout visibility',
167+
showStdIn: true,
168+
showStdout: true,
169+
},
170+
toolContext,
171+
);
172+
173+
expect(result.mode).toBe('sync');
174+
175+
// For async mode, check the process state directly
176+
const asyncResult = await shellStartTool.execute(
177+
{
178+
command: 'sleep 1',
179+
description: 'Test with stdin/stdout visibility in async mode',
180+
timeout: 50, // Force async mode
181+
showStdIn: true,
182+
showStdout: true,
183+
},
184+
toolContext,
185+
);
186+
187+
if (asyncResult.mode === 'async') {
188+
const processState = processStates.get(asyncResult.instanceId);
189+
expect(processState).toBeDefined();
190+
expect(processState?.showStdIn).toBe(true);
191+
expect(processState?.showStdout).toBe(true);
192+
}
193+
});
161194
});

packages/agent/src/tools/system/shellStart.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ type ProcessState = {
2020
signaled: boolean;
2121
exitCode: number | null;
2222
};
23+
showStdIn: boolean;
24+
showStdout: boolean;
2325
};
2426

2527
// Global map to store process state
@@ -36,6 +38,14 @@ const parameterSchema = z.object({
3638
.describe(
3739
'Timeout in ms before switching to async mode (default: 10s, which usually is sufficient)',
3840
),
41+
showStdIn: z
42+
.boolean()
43+
.optional()
44+
.describe('Whether to show the command input in the logs (default: false)'),
45+
showStdout: z
46+
.boolean()
47+
.optional()
48+
.describe('Whether to show command output in the logs (default: false)'),
3949
});
4050

4151
const returnSchema = z.union([
@@ -77,9 +87,17 @@ export const shellStartTool: Tool<Parameters, ReturnType> = {
7787
returnsJsonSchema: zodToJsonSchema(returnSchema),
7888

7989
execute: async (
80-
{ command, timeout = DEFAULT_TIMEOUT },
90+
{
91+
command,
92+
timeout = DEFAULT_TIMEOUT,
93+
showStdIn = false,
94+
showStdout = false,
95+
},
8196
{ logger, workingDirectory },
8297
): Promise<ReturnType> => {
98+
if (showStdIn) {
99+
logger.info(`Command input: ${command}`);
100+
}
83101
logger.verbose(`Starting shell command: ${command}`);
84102

85103
return new Promise((resolve) => {
@@ -100,6 +118,8 @@ export const shellStartTool: Tool<Parameters, ReturnType> = {
100118
stdout: [],
101119
stderr: [],
102120
state: { completed: false, signaled: false, exitCode: null },
121+
showStdIn,
122+
showStdout,
103123
};
104124

105125
// Initialize combined process state
@@ -111,13 +131,19 @@ export const shellStartTool: Tool<Parameters, ReturnType> = {
111131
const output = data.toString();
112132
processState.stdout.push(output);
113133
logger.verbose(`[${instanceId}] stdout: ${output.trim()}`);
134+
if (processState.showStdout) {
135+
logger.info(`[${instanceId}] stdout: ${output.trim()}`);
136+
}
114137
});
115138

116139
if (process.stderr)
117140
process.stderr.on('data', (data) => {
118141
const output = data.toString();
119142
processState.stderr.push(output);
120143
logger.verbose(`[${instanceId}] stderr: ${output.trim()}`);
144+
if (processState.showStdout) {
145+
logger.info(`[${instanceId}] stderr: ${output.trim()}`);
146+
}
121147
});
122148

123149
process.on('error', (error) => {
@@ -186,10 +212,18 @@ export const shellStartTool: Tool<Parameters, ReturnType> = {
186212
},
187213

188214
logParameters: (
189-
{ command, description, timeout = DEFAULT_TIMEOUT },
215+
{
216+
command,
217+
description,
218+
timeout = DEFAULT_TIMEOUT,
219+
showStdIn = false,
220+
showStdout = false,
221+
},
190222
{ logger },
191223
) => {
192-
logger.info(`Running "${command}", ${description} (timeout: ${timeout}ms)`);
224+
logger.info(
225+
`Running "${command}", ${description} (timeout: ${timeout}ms, showStdIn: ${showStdIn}, showStdout: ${showStdout})`,
226+
);
193227
},
194228
logReturns: (output, { logger }) => {
195229
if (output.mode === 'async') {

0 commit comments

Comments
 (0)