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:
2026-01-27 23:33:06 -05:00
commit 0062e5d9d9
521 changed files with 66418 additions and 0 deletions

188
src/tools/bash/execute.ts Normal file
View 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
View 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
View 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
View 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);
};