Terminal-based AI coding agent with interactive TUI for autonomous code generation.
Features: - Interactive TUI with React/Ink - Autonomous agent with tool calls (bash, read, write, edit, glob, grep) - Permission system with pattern-based rules - Session management with auto-compaction - Dual providers: GitHub Copilot and Ollama - MCP server integration - Todo panel and theme system - Streaming responses - GitHub-compatible project context
This commit is contained in:
188
src/tools/bash/execute.ts
Normal file
188
src/tools/bash/execute.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Bash command execution
|
||||
*/
|
||||
|
||||
import { spawn } from "child_process";
|
||||
|
||||
import {
|
||||
BASH_DEFAULTS,
|
||||
BASH_MESSAGES,
|
||||
BASH_DESCRIPTION,
|
||||
} from "@constants/bash";
|
||||
import { promptPermission } from "@services/permissions";
|
||||
import { bashParams } from "@tools/bash/params";
|
||||
import {
|
||||
truncateOutput,
|
||||
createOutputHandler,
|
||||
updateRunningStatus,
|
||||
} from "@tools/bash/output";
|
||||
import {
|
||||
createTimeoutHandler,
|
||||
createAbortHandler,
|
||||
setupAbortListener,
|
||||
removeAbortListener,
|
||||
} from "@tools/bash/process";
|
||||
import type {
|
||||
ToolDefinition,
|
||||
ToolContext,
|
||||
ToolResult,
|
||||
BashParams,
|
||||
} from "@/types/tools";
|
||||
|
||||
const createDeniedResult = (description: string): ToolResult => ({
|
||||
success: false,
|
||||
title: description,
|
||||
output: "",
|
||||
error: BASH_MESSAGES.PERMISSION_DENIED,
|
||||
});
|
||||
|
||||
const createTimeoutResult = (
|
||||
description: string,
|
||||
output: string,
|
||||
timeout: number,
|
||||
code: number | null,
|
||||
): ToolResult => ({
|
||||
success: false,
|
||||
title: description,
|
||||
output: truncateOutput(output),
|
||||
error: BASH_MESSAGES.TIMED_OUT(timeout),
|
||||
metadata: {
|
||||
exitCode: code,
|
||||
timedOut: true,
|
||||
},
|
||||
});
|
||||
|
||||
const createAbortedResult = (
|
||||
description: string,
|
||||
output: string,
|
||||
code: number | null,
|
||||
): ToolResult => ({
|
||||
success: false,
|
||||
title: description,
|
||||
output: truncateOutput(output),
|
||||
error: BASH_MESSAGES.ABORTED,
|
||||
metadata: {
|
||||
exitCode: code,
|
||||
aborted: true,
|
||||
},
|
||||
});
|
||||
|
||||
const createCompletedResult = (
|
||||
description: string,
|
||||
output: string,
|
||||
code: number | null,
|
||||
): ToolResult => ({
|
||||
success: code === 0,
|
||||
title: description,
|
||||
output: truncateOutput(output),
|
||||
error: code !== 0 ? BASH_MESSAGES.EXIT_CODE(code ?? -1) : undefined,
|
||||
metadata: {
|
||||
exitCode: code,
|
||||
},
|
||||
});
|
||||
|
||||
const createErrorResult = (
|
||||
description: string,
|
||||
output: string,
|
||||
error: Error,
|
||||
): ToolResult => ({
|
||||
success: false,
|
||||
title: description,
|
||||
output,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
const checkPermission = async (
|
||||
command: string,
|
||||
description: string,
|
||||
autoApprove: boolean,
|
||||
): Promise<boolean> => {
|
||||
if (autoApprove) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const result = await promptPermission(command, description);
|
||||
return result.allowed;
|
||||
};
|
||||
|
||||
const executeCommand = (
|
||||
args: BashParams,
|
||||
ctx: ToolContext,
|
||||
): Promise<ToolResult> => {
|
||||
const {
|
||||
command,
|
||||
description,
|
||||
workdir,
|
||||
timeout = BASH_DEFAULTS.TIMEOUT,
|
||||
} = args;
|
||||
const cwd = workdir ?? ctx.workingDir;
|
||||
|
||||
updateRunningStatus(ctx, description);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn(command, {
|
||||
shell: process.env.SHELL ?? "/bin/bash",
|
||||
cwd,
|
||||
env: { ...process.env },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const outputRef = { value: "" };
|
||||
const timedOutRef = { value: false };
|
||||
|
||||
const appendOutput = createOutputHandler(ctx, description, outputRef);
|
||||
proc.stdout?.on("data", appendOutput);
|
||||
proc.stderr?.on("data", appendOutput);
|
||||
|
||||
const timeoutId = createTimeoutHandler(proc, timeout, timedOutRef);
|
||||
const abortHandler = createAbortHandler(proc);
|
||||
setupAbortListener(ctx, abortHandler);
|
||||
|
||||
proc.on("close", (code) => {
|
||||
clearTimeout(timeoutId);
|
||||
removeAbortListener(ctx, abortHandler);
|
||||
|
||||
if (timedOutRef.value) {
|
||||
resolve(
|
||||
createTimeoutResult(description, outputRef.value, timeout, code),
|
||||
);
|
||||
} else if (ctx.abort.signal.aborted) {
|
||||
resolve(createAbortedResult(description, outputRef.value, code));
|
||||
} else {
|
||||
resolve(createCompletedResult(description, outputRef.value, code));
|
||||
}
|
||||
});
|
||||
|
||||
proc.on("error", (error) => {
|
||||
clearTimeout(timeoutId);
|
||||
removeAbortListener(ctx, abortHandler);
|
||||
resolve(createErrorResult(description, outputRef.value, error));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const executeBash = async (
|
||||
args: BashParams,
|
||||
ctx: ToolContext,
|
||||
): Promise<ToolResult> => {
|
||||
const { command, description } = args;
|
||||
|
||||
const allowed = await checkPermission(
|
||||
command,
|
||||
description,
|
||||
ctx.autoApprove ?? false,
|
||||
);
|
||||
|
||||
if (!allowed) {
|
||||
return createDeniedResult(description);
|
||||
}
|
||||
|
||||
return executeCommand(args, ctx);
|
||||
};
|
||||
|
||||
export const bashTool: ToolDefinition<typeof bashParams> = {
|
||||
name: "bash",
|
||||
description: BASH_DESCRIPTION,
|
||||
parameters: bashParams,
|
||||
execute: executeBash,
|
||||
};
|
||||
39
src/tools/bash/output.ts
Normal file
39
src/tools/bash/output.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Bash output handling utilities
|
||||
*/
|
||||
|
||||
import { BASH_DEFAULTS, BASH_MESSAGES } from "@constants/bash";
|
||||
import type { ToolContext } from "@/types/tools";
|
||||
|
||||
export const truncateOutput = (output: string): string =>
|
||||
output.length > BASH_DEFAULTS.MAX_OUTPUT_LENGTH
|
||||
? output.slice(0, BASH_DEFAULTS.MAX_OUTPUT_LENGTH) + BASH_MESSAGES.TRUNCATED
|
||||
: output;
|
||||
|
||||
export const createOutputHandler = (
|
||||
ctx: ToolContext,
|
||||
description: string,
|
||||
outputRef: { value: string },
|
||||
) => {
|
||||
return (data: Buffer): void => {
|
||||
const chunk = data.toString();
|
||||
outputRef.value += chunk;
|
||||
|
||||
ctx.onMetadata?.({
|
||||
title: description,
|
||||
status: "running",
|
||||
output: truncateOutput(outputRef.value),
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const updateRunningStatus = (
|
||||
ctx: ToolContext,
|
||||
description: string,
|
||||
): void => {
|
||||
ctx.onMetadata?.({
|
||||
title: description,
|
||||
status: "running",
|
||||
output: "",
|
||||
});
|
||||
};
|
||||
24
src/tools/bash/params.ts
Normal file
24
src/tools/bash/params.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Bash tool parameter schema
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const bashParams = z.object({
|
||||
command: z.string().describe("The bash command to execute"),
|
||||
description: z
|
||||
.string()
|
||||
.describe("A brief description of what this command does"),
|
||||
workdir: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Working directory for the command (defaults to current directory)",
|
||||
),
|
||||
timeout: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Timeout in milliseconds (default: 120000)"),
|
||||
});
|
||||
|
||||
export type BashParamsSchema = typeof bashParams;
|
||||
40
src/tools/bash/process.ts
Normal file
40
src/tools/bash/process.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Bash process management utilities
|
||||
*/
|
||||
|
||||
import type { ChildProcess } from "child_process";
|
||||
|
||||
import { BASH_DEFAULTS, BASH_SIGNALS } from "@constants/bash";
|
||||
|
||||
export const killProcess = (proc: ChildProcess): void => {
|
||||
proc.kill(BASH_SIGNALS.TERMINATE);
|
||||
setTimeout(() => proc.kill(BASH_SIGNALS.KILL), BASH_DEFAULTS.KILL_DELAY);
|
||||
};
|
||||
|
||||
export const createTimeoutHandler = (
|
||||
proc: ChildProcess,
|
||||
timeout: number,
|
||||
timedOutRef: { value: boolean },
|
||||
): NodeJS.Timeout =>
|
||||
setTimeout(() => {
|
||||
timedOutRef.value = true;
|
||||
killProcess(proc);
|
||||
}, timeout);
|
||||
|
||||
export const createAbortHandler = (proc: ChildProcess) => (): void => {
|
||||
killProcess(proc);
|
||||
};
|
||||
|
||||
export const setupAbortListener = (
|
||||
ctx: { abort: AbortController },
|
||||
handler: () => void,
|
||||
): void => {
|
||||
ctx.abort.signal.addEventListener("abort", handler, { once: true });
|
||||
};
|
||||
|
||||
export const removeAbortListener = (
|
||||
ctx: { abort: AbortController },
|
||||
handler: () => void,
|
||||
): void => {
|
||||
ctx.abort.signal.removeEventListener("abort", handler);
|
||||
};
|
||||
Reference in New Issue
Block a user