Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "1.3.8",
"version": "1.3.9",
"description": "Generic bash tool for AI agents, compatible with AI SDK",
"type": "module",
"main": "dist/index.js",
Expand Down
150 changes: 150 additions & 0 deletions src/tool.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,156 @@ describe("createBashTool integration", () => {

expect(result.content).toBe('export const hello = "world";');
});

it("applies outputFilter to file content", async () => {
const { tools } = await createBashTool({
files: {
"multiline.txt": "line1\nline2\nline3\nline4\nline5",
},
});

assert(tools.readFile.execute, "readFile.execute should be defined");
const result = (await tools.readFile.execute(
{ path: "multiline.txt", outputFilter: "tail -2" },
opts,
)) as { content: string };

expect(result.content.trim()).toBe("line4\nline5");
});

it("applies grep filter to file content", async () => {
const { tools } = await createBashTool({
files: {
"log.txt": "INFO: started\nERROR: failed\nINFO: done\nERROR: timeout",
},
});

assert(tools.readFile.execute, "readFile.execute should be defined");
const result = (await tools.readFile.execute(
{ path: "log.txt", outputFilter: "grep ERROR" },
opts,
)) as { content: string };

expect(result.content).toContain("ERROR: failed");
expect(result.content).toContain("ERROR: timeout");
expect(result.content).not.toContain("INFO");
});
});

describe("outputFilter", () => {
it("filters bash command output with tail", async () => {
const { tools } = await createBashTool({
files: {
"numbers.txt": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10",
},
});

assert(tools.bash.execute, "bash.execute should be defined");
const result = (await tools.bash.execute(
{ command: "cat numbers.txt", outputFilter: "tail -3" },
opts,
)) as CommandResult;

expect(result.exitCode).toBe(0);
expect(result.stdout.trim()).toBe("8\n9\n10");
});

it("filters bash command output with grep", async () => {
const { tools } = await createBashTool({
files: testFiles,
});

assert(tools.bash.execute, "bash.execute should be defined");
const result = (await tools.bash.execute(
{ command: "find . -type f", outputFilter: "grep -E '\\.ts$'" },
opts,
)) as CommandResult;

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("index.ts");
expect(result.stdout).toContain("helpers.ts");
expect(result.stdout).not.toContain("package.json");
});

it("filters bash command output with head", async () => {
const { tools } = await createBashTool({
files: {
"data.txt": "a\nb\nc\nd\ne",
},
});

assert(tools.bash.execute, "bash.execute should be defined");
const result = (await tools.bash.execute(
{ command: "cat data.txt", outputFilter: "head -2" },
opts,
)) as CommandResult;

expect(result.exitCode).toBe(0);
expect(result.stdout.trim()).toBe("a\nb");
});
});

describe("invocation logging", () => {
it("stores full output and returns filtered output", async () => {
const { tools } = await createBashTool({
files: {
"numbers.txt": "1\n2\n3\n4\n5",
},
enableInvocationLog: true,
});

assert(tools.bash.execute, "bash.execute should be defined");
assert(tools.readFile.execute, "readFile.execute should be defined");

const result = (await tools.bash.execute(
{ command: "cat numbers.txt", outputFilter: "tail -2" },
opts,
)) as CommandResult & { invocationLogPath: string };

// Filtered output returned
expect(result.stdout.trim()).toBe("4\n5");
expect(result.invocationLogPath).toMatch(/\.invocation$/);

// Full output available in log
const logResult = (await tools.readFile.execute(
{ path: result.invocationLogPath },
opts,
)) as { content: string };

expect(logResult.content).toContain("1\n2\n3\n4\n5");
});

it("allows re-filtering invocation log", async () => {
const { tools } = await createBashTool({
files: {
"log.txt":
"INFO: start\nERROR: fail1\nINFO: middle\nERROR: fail2\nINFO: end",
},
enableInvocationLog: true,
});

assert(tools.bash.execute, "bash.execute should be defined");
assert(tools.readFile.execute, "readFile.execute should be defined");

// First, get last 2 lines
const result = (await tools.bash.execute(
{ command: "cat log.txt", outputFilter: "tail -2" },
opts,
)) as CommandResult & { invocationLogPath: string };

expect(result.stdout).toContain("fail2");
expect(result.stdout).toContain("end");

// Now re-filter the full log for errors only
const errorResult = (await tools.readFile.execute(
{ path: result.invocationLogPath, outputFilter: "grep ERROR" },
opts,
)) as { content: string };

expect(errorResult.content).toContain("ERROR: fail1");
expect(errorResult.content).toContain("ERROR: fail2");
expect(errorResult.content).not.toContain("INFO");
});
});

describe("writeFile tool", () => {
Expand Down
41 changes: 34 additions & 7 deletions src/tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,15 @@ describe("createBashTool", () => {
});
});

// Common description sections for tests
const OUTPUT_FILTERING_SECTION = `OUTPUT FILTERING:
Use the outputFilter parameter to filter stdout before it is returned.
Examples:
outputFilter: "tail -50" # Last 50 lines
outputFilter: "head -100" # First 100 lines
outputFilter: "grep error" # Lines containing "error"
outputFilter: "grep -i warn" # Case-insensitive search`;

describe("createBashTool tool prompt integration", () => {
beforeEach(() => {
for (const key of Object.keys(mockFiles)) {
Expand Down Expand Up @@ -389,7 +398,9 @@ Common operations:
ls -la # List files with details
find . -name '*.ts' # Find files by pattern
grep -r 'pattern' . # Search file contents
cat <file> # View file contents`);
cat <file> # View file contents

${OUTPUT_FILTERING_SECTION}`);
});

it("includes format-specific hints for JSON files", async () => {
Expand All @@ -414,7 +425,9 @@ Common operations:
ls -la # List files with details
find . -name '*.ts' # Find files by pattern
grep -r 'pattern' . # Search file contents
cat <file> # View file contents`);
cat <file> # View file contents

${OUTPUT_FILTERING_SECTION}`);
});

it("includes format-specific hints for YAML files", async () => {
Expand All @@ -439,7 +452,9 @@ Common operations:
ls -la # List files with details
find . -name '*.ts' # Find files by pattern
grep -r 'pattern' . # Search file contents
cat <file> # View file contents`);
cat <file> # View file contents

${OUTPUT_FILTERING_SECTION}`);
});

it("includes format-specific hints for multiple formats", async () => {
Expand Down Expand Up @@ -471,7 +486,9 @@ Common operations:
ls -la # List files with details
find . -name '*.ts' # Find files by pattern
grep -r 'pattern' . # Search file contents
cat <file> # View file contents`);
cat <file> # View file contents

${OUTPUT_FILTERING_SECTION}`);
});

it("includes yq for CSV when using just-bash sandbox", async () => {
Expand All @@ -497,7 +514,9 @@ Common operations:
ls -la # List files with details
find . -name '*.ts' # Find files by pattern
grep -r 'pattern' . # Search file contents
cat <file> # View file contents`);
cat <file> # View file contents

${OUTPUT_FILTERING_SECTION}`);
});

it("includes extraInstructions after tool prompt", async () => {
Expand All @@ -524,6 +543,8 @@ Common operations:
grep -r 'pattern' . # Search file contents
cat <file> # View file contents

${OUTPUT_FILTERING_SECTION}

Always use TypeScript.`);
});

Expand Down Expand Up @@ -563,7 +584,9 @@ Common operations:
ls -la # List files with details
find . -name '*.ts' # Find files by pattern
grep -r 'pattern' . # Search file contents
cat <file> # View file contents`);
cat <file> # View file contents

${OUTPUT_FILTERING_SECTION}`);
});

it("uses empty string toolPrompt to disable tool hints", async () => {
Expand All @@ -588,7 +611,9 @@ Common operations:
ls -la # List files with details
find . -name '*.ts' # Find files by pattern
grep -r 'pattern' . # Search file contents
cat <file> # View file contents`);
cat <file> # View file contents

${OUTPUT_FILTERING_SECTION}`);
});

it("combines custom toolPrompt with extraInstructions", async () => {
Expand Down Expand Up @@ -618,6 +643,8 @@ Common operations:
grep -r 'pattern' . # Search file contents
cat <file> # View file contents

${OUTPUT_FILTERING_SECTION}

Always run tests first.`);
});
});
Expand Down
2 changes: 2 additions & 0 deletions src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ export async function createBashTool(
onBeforeBashCall: options.onBeforeBashCall,
onAfterBashCall: options.onAfterBashCall,
maxOutputLength: options.maxOutputLength,
enableInvocationLog: options.enableInvocationLog,
invocationLogPath: options.invocationLogPath,
});

const tools = {
Expand Down
Loading