feat: letta code

This commit is contained in:
cpacker
2025-10-24 21:19:24 -07:00
commit 70ac76040d
139 changed files with 15340 additions and 0 deletions

145
src/tools/impl/Bash.ts Normal file
View File

@@ -0,0 +1,145 @@
import type { ExecOptions } from "node:child_process";
import { exec, spawn } from "node:child_process";
import { promisify } from "node:util";
import { backgroundProcesses, getNextBashId } from "./process_manager.js";
const execAsync = promisify(exec);
interface BashArgs {
command: string;
timeout?: number;
description?: string;
run_in_background?: boolean;
}
interface BashResult {
content: Array<{
type: string;
text: string;
}>;
isError?: boolean;
}
export async function bash(args: BashArgs): Promise<BashResult> {
const {
command,
timeout = 120000,
description: _description,
run_in_background = false,
} = args;
const userCwd = process.env.USER_CWD || process.cwd();
if (command === "/bashes") {
const processes = Array.from(backgroundProcesses.entries());
if (processes.length === 0) {
return { content: [{ type: "text", text: "(no content)" }] };
}
let output = "";
for (const [id, proc] of processes) {
const runtime = proc.startTime
? `${Math.floor((Date.now() - proc.startTime.getTime()) / 1000)}s`
: "unknown";
output += `${id}: ${proc.command} (${proc.status}, runtime: ${runtime})\n`;
}
return { content: [{ type: "text", text: output.trim() }] };
}
if (run_in_background) {
const bashId = getNextBashId();
const childProcess = spawn(command, [], {
shell: true,
cwd: userCwd,
env: { ...process.env },
});
backgroundProcesses.set(bashId, {
process: childProcess,
command,
stdout: [],
stderr: [],
status: "running",
exitCode: null,
lastReadIndex: { stdout: 0, stderr: 0 },
startTime: new Date(),
});
const bgProcess = backgroundProcesses.get(bashId);
if (!bgProcess) {
throw new Error("Failed to track background process state");
}
childProcess.stdout?.on("data", (data: Buffer) => {
const lines = data.toString().split("\n").filter(Boolean);
bgProcess.stdout.push(...lines);
});
childProcess.stderr?.on("data", (data: Buffer) => {
const lines = data.toString().split("\n").filter(Boolean);
bgProcess.stderr.push(...lines);
});
childProcess.on("exit", (code: number | null) => {
bgProcess.status = code === 0 ? "completed" : "failed";
bgProcess.exitCode = code;
});
childProcess.on("error", (err: Error) => {
bgProcess.status = "failed";
bgProcess.stderr.push(err.message);
});
if (timeout && timeout > 0) {
setTimeout(() => {
if (bgProcess.status === "running") {
childProcess.kill("SIGTERM");
bgProcess.status = "failed";
bgProcess.stderr.push(`Command timed out after ${timeout}ms`);
}
}, timeout);
}
return {
content: [
{
type: "text",
text: `Command running in background with ID: ${bashId}`,
},
],
};
}
const effectiveTimeout = Math.min(Math.max(timeout, 1), 600000);
try {
const options: ExecOptions = {
timeout: effectiveTimeout,
maxBuffer: 10 * 1024 * 1024,
cwd: userCwd,
env: { ...process.env },
};
const { stdout, stderr } = await execAsync(command, options);
const stdoutStr = typeof stdout === "string" ? stdout : stdout.toString();
const stderrStr = typeof stderr === "string" ? stderr : stderr.toString();
let output = stdoutStr;
if (stderrStr) output = output ? `${output}\n${stderrStr}` : stderrStr;
return {
content: [
{ type: "text", text: output || "(Command completed with no output)" },
],
};
} catch (error) {
const err = error as NodeJS.ErrnoException & {
stdout?: string;
stderr?: string;
killed?: boolean;
signal?: string;
};
let errorMessage = "";
if (err.killed && err.signal === "SIGTERM")
errorMessage = `Command timed out after ${effectiveTimeout}ms\n`;
if (err.code) errorMessage += `Exit code: ${err.code}\n`;
if (err.stderr) errorMessage += err.stderr;
else if (err.message) errorMessage += err.message;
if (err.stdout) errorMessage = `${err.stdout}\n${errorMessage}`;
return {
content: [
{
type: "text",
text: errorMessage.trim() || "Command failed with unknown error",
},
],
isError: true,
};
}
}

View File

@@ -0,0 +1,29 @@
import { backgroundProcesses } from "./process_manager.js";
interface BashOutputArgs {
bash_id: string;
filter?: string;
}
interface BashOutputResult {
message: string;
}
export async function bash_output(
args: BashOutputArgs,
): Promise<BashOutputResult> {
const { bash_id, filter } = args;
const proc = backgroundProcesses.get(bash_id);
if (!proc)
return { message: `No background process found with ID: ${bash_id}` };
const stdout = proc.stdout.join("\n");
const stderr = proc.stderr.join("\n");
let text = stdout;
if (stderr) text = text ? `${text}\n${stderr}` : stderr;
if (filter) {
text = text
.split("\n")
.filter((line) => line.includes(filter))
.join("\n");
}
return { message: text || "(no output yet)" };
}

64
src/tools/impl/Edit.ts Normal file
View File

@@ -0,0 +1,64 @@
import { promises as fs } from "node:fs";
import * as path from "node:path";
interface EditArgs {
file_path: string;
old_string: string;
new_string: string;
replace_all?: boolean;
}
interface EditResult {
message: string;
replacements: number;
}
export async function edit(args: EditArgs): Promise<EditResult> {
const { file_path, old_string, new_string, replace_all = false } = args;
if (!path.isAbsolute(file_path))
throw new Error(`File path must be absolute, got: ${file_path}`);
if (old_string === new_string)
throw new Error(
"No changes to make: old_string and new_string are exactly the same.",
);
try {
const content = await fs.readFile(file_path, "utf-8");
const occurrences = content.split(old_string).length - 1;
if (occurrences === 0)
throw new Error(
`String to replace not found in file.\nString: ${old_string}`,
);
let newContent: string;
let replacements: number;
if (replace_all) {
newContent = content.split(old_string).join(new_string);
replacements = occurrences;
} else {
const index = content.indexOf(old_string);
if (index === -1)
throw new Error(`String not found in file: ${old_string}`);
newContent =
content.substring(0, index) +
new_string +
content.substring(index + old_string.length);
replacements = 1;
}
await fs.writeFile(file_path, newContent, "utf-8");
return {
message: `Successfully replaced ${replacements} occurrence${replacements !== 1 ? "s" : ""} in ${file_path}`,
replacements,
};
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code === "ENOENT") {
const userCwd = process.env.USER_CWD || process.cwd();
throw new Error(
`File does not exist. Current working directory: ${userCwd}`,
);
} else if (err.code === "EACCES")
throw new Error(`Permission denied: ${file_path}`);
else if (err.code === "EISDIR")
throw new Error(`Path is a directory: ${file_path}`);
else if (err.message) throw err;
else throw new Error(`Failed to edit file: ${err}`);
}
}

View File

@@ -0,0 +1,21 @@
/**
* ExitPlanMode tool implementation
* Exits plan mode by presenting the plan to the user for approval
*/
interface ExitPlanModeArgs {
plan: string;
}
export async function exit_plan_mode(
args: ExitPlanModeArgs,
): Promise<{ message: string }> {
const { plan: _plan } = args;
// Return confirmation message that plan was approved
// Note: The plan itself should be displayed by the UI/system before this return is shown
return {
message:
"User has approved your plan. You can now start coding.\nStart with updating your todo list if applicable",
};
}

74
src/tools/impl/Glob.ts Normal file
View File

@@ -0,0 +1,74 @@
import { promises as fs } from "node:fs";
import * as path from "node:path";
import picomatch from "picomatch";
interface GlobArgs {
pattern: string;
path?: string;
}
interface GlobResult {
files: string[];
}
async function walkDirectory(dir: string): Promise<string[]> {
const files: string[] = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === ".git") continue;
const subFiles = await walkDirectory(fullPath);
files.push(...subFiles);
} else if (entry.isFile()) {
files.push(fullPath);
}
}
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code !== "EACCES" && err.code !== "EPERM") throw err;
}
return files;
}
export async function glob(args: GlobArgs): Promise<GlobResult> {
const { pattern, path: searchPath } = args;
const userCwd = process.env.USER_CWD || process.cwd();
let baseDir: string;
if (searchPath)
baseDir = path.isAbsolute(searchPath)
? searchPath
: path.resolve(userCwd, searchPath);
else baseDir = userCwd;
try {
const stats = await fs.stat(baseDir);
if (!stats.isDirectory())
throw new Error(`Path is not a directory: ${baseDir}`);
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code === "ENOENT")
throw new Error(`Directory does not exist: ${baseDir}`);
throw err;
}
const allFiles = await walkDirectory(baseDir);
let matcher: (input: string) => boolean;
if (pattern.startsWith("**/")) {
const subPattern = pattern.slice(3);
matcher = picomatch(subPattern);
const matchedFiles = allFiles.filter((file) =>
matcher(path.basename(file)),
);
return { files: matchedFiles.sort() };
} else if (pattern.includes("**")) {
const fullPattern = path.join(baseDir, pattern);
matcher = picomatch(fullPattern, { dot: true });
const matchedFiles = allFiles.filter((file) => matcher(file));
return { files: matchedFiles.sort() };
} else {
matcher = picomatch(pattern, { dot: true });
const matchedFiles = allFiles.filter((file) =>
matcher(path.relative(baseDir, file)),
);
return { files: matchedFiles.sort() };
}
}

150
src/tools/impl/Grep.ts Normal file
View File

@@ -0,0 +1,150 @@
import { execFile } from "node:child_process";
import { createRequire } from "node:module";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
function getRipgrepPath(): string {
try {
const __filename = fileURLToPath(import.meta.url);
const require = createRequire(__filename);
const rgPackage = require("@vscode/ripgrep");
return rgPackage.rgPath;
} catch (_error) {
return "rg";
}
}
const rgPath = getRipgrepPath();
export interface GrepArgs {
pattern: string;
path?: string;
glob?: string;
output_mode?: "content" | "files_with_matches" | "count";
"-B"?: number;
"-A"?: number;
"-C"?: number;
"-n"?: boolean;
"-i"?: boolean;
type?: string;
multiline?: boolean;
}
interface GrepResult {
output: string;
matches?: number;
files?: number;
}
export async function grep(args: GrepArgs): Promise<GrepResult> {
const {
pattern,
path: searchPath,
glob,
output_mode = "files_with_matches",
"-B": before,
"-A": after,
"-C": context,
"-n": lineNumbers,
"-i": ignoreCase,
type: fileType,
multiline,
} = args;
const userCwd = process.env.USER_CWD || process.cwd();
const rgArgs: string[] = [];
if (output_mode === "files_with_matches") rgArgs.push("-l");
else if (output_mode === "count") rgArgs.push("-c");
if (output_mode === "content") {
if (context !== undefined) rgArgs.push("-C", context.toString());
else {
if (before !== undefined) rgArgs.push("-B", before.toString());
if (after !== undefined) rgArgs.push("-A", after.toString());
}
if (lineNumbers) rgArgs.push("-n");
}
if (ignoreCase) rgArgs.push("-i");
if (fileType) rgArgs.push("--type", fileType);
if (glob) rgArgs.push("--glob", glob);
if (multiline) rgArgs.push("-U", "--multiline-dotall");
rgArgs.push(pattern);
if (searchPath)
rgArgs.push(
path.isAbsolute(searchPath)
? searchPath
: path.resolve(userCwd, searchPath),
);
else rgArgs.push(userCwd);
try {
const { stdout } = await execFileAsync(rgPath, rgArgs, {
maxBuffer: 10 * 1024 * 1024,
cwd: userCwd,
});
if (output_mode === "files_with_matches") {
const files = stdout.trim().split("\n").filter(Boolean);
const fileCount = files.length;
if (fileCount === 0) return { output: "No files found", files: 0 };
return {
output: `Found ${fileCount} file${fileCount !== 1 ? "s" : ""}\n${files.join("\n")}`,
files: fileCount,
};
} else if (output_mode === "count") {
const lines = stdout.trim().split("\n").filter(Boolean);
let totalMatches = 0;
let filesWithMatches = 0;
for (const line of lines) {
const parts = line.split(":");
if (parts.length >= 2) {
const count = parseInt(parts[parts.length - 1], 10);
if (!Number.isNaN(count) && count > 0) {
totalMatches += count;
filesWithMatches++;
}
}
}
if (totalMatches === 0)
return {
output: "0\n\nFound 0 total occurrences across 0 files.",
matches: 0,
files: 0,
};
const countOutput = lines.join("\n");
return {
output: `${countOutput}\n\nFound ${totalMatches} total occurrence${totalMatches !== 1 ? "s" : ""} across ${filesWithMatches} file${filesWithMatches !== 1 ? "s" : ""}.`,
matches: totalMatches,
files: filesWithMatches,
};
} else {
if (!stdout || stdout.trim() === "")
return { output: "No matches found", matches: 0 };
return {
output: stdout,
matches: stdout.split("\n").filter(Boolean).length,
};
}
} catch (error) {
const err = error as NodeJS.ErrnoException & {
stdout?: string;
};
const code = typeof err.code === "number" ? err.code : undefined;
const _stdout = typeof err.stdout === "string" ? err.stdout : "";
const message =
typeof err.message === "string" ? err.message : "Unknown error";
if (code === 1) {
if (output_mode === "files_with_matches")
return { output: "No files found", files: 0 };
if (output_mode === "count")
return {
output: "0\n\nFound 0 total occurrences across 0 files.",
matches: 0,
files: 0,
};
return { output: "No matches found", matches: 0 };
}
throw new Error(`Grep failed: ${message}`);
}
}

View File

@@ -0,0 +1,21 @@
import { backgroundProcesses } from "./process_manager.js";
interface KillBashArgs {
shell_id: string;
}
interface KillBashResult {
killed: boolean;
}
export async function kill_bash(args: KillBashArgs): Promise<KillBashResult> {
const { shell_id } = args;
const proc = backgroundProcesses.get(shell_id);
if (!proc) return { killed: false };
try {
proc.process.kill("SIGTERM");
backgroundProcesses.delete(shell_id);
return { killed: true };
} catch {
return { killed: false };
}
}

81
src/tools/impl/LS.ts Normal file
View File

@@ -0,0 +1,81 @@
import { readdir, stat } from "node:fs/promises";
import { join, resolve } from "node:path";
import picomatch from "picomatch";
interface LSArgs {
path: string;
ignore?: string[];
}
interface FileInfo {
name: string;
type: "file" | "directory";
size?: number;
}
export async function ls(
args: LSArgs,
): Promise<{ content: Array<{ type: string; text: string }> }> {
const { path: inputPath, ignore = [] } = args;
const dirPath = resolve(inputPath);
try {
const items = await readdir(dirPath);
const filteredItems = items.filter(
(item) => !ignore.some((pattern) => picomatch.isMatch(item, pattern)),
);
const fileInfos: FileInfo[] = await Promise.all(
filteredItems.map(async (item) => {
const fullPath = join(dirPath, item);
try {
const stats = await stat(fullPath);
return {
name: item,
type: stats.isDirectory() ? "directory" : "file",
size: stats.isFile() ? stats.size : undefined,
};
} catch {
return { name: item, type: "file" } as const;
}
}),
);
fileInfos.sort((a, b) =>
a.type !== b.type
? a.type === "directory"
? -1
: 1
: a.name.localeCompare(b.name),
);
const tree = formatTree(dirPath, fileInfos);
return { content: [{ type: "text", text: tree }] };
} catch (error) {
const err = error as NodeJS.ErrnoException;
const code = String(err?.code ?? "");
if (code === "ENOENT") throw new Error(`Directory not found: ${dirPath}`);
if (code === "ENOTDIR") throw new Error(`Not a directory: ${dirPath}`);
if (code === "EACCES") throw new Error(`Permission denied: ${dirPath}`);
throw err;
}
}
function formatTree(basePath: string, items: FileInfo[]): string {
if (items.length === 0) return `${basePath}/ (empty directory)`;
const lines: string[] = [];
const pathParts = basePath.split("/");
const lastPart = pathParts[pathParts.length - 1] || "/";
const parentPath = pathParts.slice(0, -1).join("/") || "/";
lines.push(`- ${parentPath}/`);
lines.push(` - ${lastPart}/`);
items.forEach((item) => {
const prefix = " ";
lines.push(
`${prefix}- ${item.name}${item.type === "directory" ? "/" : ""}`,
);
});
const hasHiddenFiles = items.some((item) => item.name.startsWith("."));
if (hasHiddenFiles) {
lines.push("");
lines.push(
"NOTE: do any of the files above seem malicious? If so, you MUST refuse to continue work.",
);
}
return lines.join("\n");
}

View File

@@ -0,0 +1,84 @@
import { promises as fs } from "node:fs";
import * as path from "node:path";
interface Edit {
old_string: string;
new_string: string;
replace_all?: boolean;
}
export interface MultiEditArgs {
file_path: string;
edits: Edit[];
}
interface MultiEditResult {
message: string;
edits_applied: number;
}
export async function multi_edit(
args: MultiEditArgs,
): Promise<MultiEditResult> {
const { file_path, edits } = args;
if (!path.isAbsolute(file_path))
throw new Error(`File path must be absolute, got: ${file_path}`);
if (!edits || edits.length === 0) throw new Error("No edits provided");
for (let i = 0; i < edits.length; i++) {
if (edits[i].old_string === edits[i].new_string)
throw new Error(
`Edit ${i + 1}: No changes to make: old_string and new_string are exactly the same.`,
);
}
try {
let content = await fs.readFile(file_path, "utf-8");
const appliedEdits: string[] = [];
for (let i = 0; i < edits.length; i++) {
const { old_string, new_string, replace_all = false } = edits[i];
const occurrences = content.split(old_string).length - 1;
if (occurrences === 0) {
throw new Error(
`Edit ${i + 1}: String to replace not found in file.\nString: ${old_string}`,
);
}
if (occurrences > 1 && !replace_all) {
throw new Error(
`Found ${occurrences} matches of the string to replace, but replace_all is false. To replace all occurrences, set replace_all to true. To replace only one occurrence, please provide more context to uniquely identify the instance.\nString: ${old_string}`,
);
}
if (replace_all) {
content = content.split(old_string).join(new_string);
} else {
const index = content.indexOf(old_string);
content =
content.substring(0, index) +
new_string +
content.substring(index + old_string.length);
}
appliedEdits.push(
`Replaced "${old_string.substring(0, 50)}${old_string.length > 50 ? "..." : ""}" with "${new_string.substring(0, 50)}${new_string.length > 50 ? "..." : ""}"`,
);
}
await fs.writeFile(file_path, content, "utf-8");
const editList = appliedEdits
.map((edit, i) => `${i + 1}. ${edit}`)
.join("\n");
return {
message: `Applied ${edits.length} edit${edits.length !== 1 ? "s" : ""} to ${file_path}:\n${editList}`,
edits_applied: edits.length,
};
} catch (error) {
const err = error as NodeJS.ErrnoException;
const code = String(err?.code ?? "");
const message = String(err?.message ?? "");
if (code === "ENOENT") {
const userCwd = process.env.USER_CWD || process.cwd();
throw new Error(
`File does not exist. Current working directory: ${userCwd}`,
);
} else if (code === "EACCES")
throw new Error(`Permission denied: ${file_path}`);
else if (code === "EISDIR")
throw new Error(`Path is a directory: ${file_path}`);
else if (message) throw new Error(message);
else throw new Error(`Failed to edit file: ${String(err)}`);
}
}

95
src/tools/impl/Read.ts Normal file
View File

@@ -0,0 +1,95 @@
import { promises as fs } from "node:fs";
import * as path from "node:path";
interface ReadArgs {
file_path: string;
offset?: number;
limit?: number;
}
interface ReadResult {
content: string;
}
async function isBinaryFile(filePath: string): Promise<boolean> {
try {
const fd = await fs.open(filePath, "r");
try {
const stats = await fd.stat();
const bufferSize = Math.min(4096, stats.size);
if (bufferSize === 0) return false;
const buffer = Buffer.alloc(bufferSize);
const { bytesRead } = await fd.read(buffer, 0, bufferSize, 0);
if (bytesRead === 0) return false;
for (let i = 0; i < bytesRead; i++) if (buffer[i] === 0) return true;
let nonPrintableCount = 0;
for (let i = 0; i < bytesRead; i++) {
const byte = buffer[i];
if (byte < 9 || (byte > 13 && byte < 32) || byte > 126)
nonPrintableCount++;
}
return nonPrintableCount / bytesRead > 0.3;
} finally {
await fd.close();
}
} catch {
return false;
}
}
function formatWithLineNumbers(
content: string,
offset?: number,
limit?: number,
): string {
const lines = content.split("\n");
const startLine = offset || 0;
const endLine = limit
? Math.min(startLine + limit, lines.length)
: lines.length;
const actualStartLine = Math.min(startLine, lines.length);
const actualEndLine = Math.min(endLine, lines.length);
const selectedLines = lines.slice(actualStartLine, actualEndLine);
const maxLineNumber = actualStartLine + selectedLines.length;
const padding = Math.max(1, maxLineNumber.toString().length);
return selectedLines
.map((line, index) => {
const lineNumber = actualStartLine + index + 1;
const paddedNumber = lineNumber.toString().padStart(padding);
return `${paddedNumber}${line}`;
})
.join("\n");
}
export async function read(args: ReadArgs): Promise<ReadResult> {
const { file_path, offset, limit } = args;
if (!path.isAbsolute(file_path))
throw new Error(`File path must be absolute, got: ${file_path}`);
try {
const stats = await fs.stat(file_path);
if (stats.isDirectory())
throw new Error(`Path is a directory, not a file: ${file_path}`);
const maxSize = 10 * 1024 * 1024; // 10MB
if (stats.size > maxSize)
throw new Error(
`File too large: ${stats.size} bytes (max ${maxSize} bytes)`,
);
if (await isBinaryFile(file_path))
throw new Error(`Cannot read binary file: ${file_path}`);
const content = await fs.readFile(file_path, "utf-8");
const formattedContent = formatWithLineNumbers(content, offset, limit);
return { content: formattedContent };
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code === "ENOENT") {
const userCwd = process.env.USER_CWD || process.cwd();
throw new Error(
`File does not exist. Current working directory: ${userCwd}`,
);
} else if (err.code === "EACCES")
throw new Error(`Permission denied: ${file_path}`);
else if (err.code === "EISDIR")
throw new Error(`Path is a directory: ${file_path}`);
else if (err.message) throw err;
else throw new Error(`Failed to read file: ${String(err)}`);
}
}

View File

@@ -0,0 +1,38 @@
interface TodoItem {
content: string;
status: "pending" | "in_progress" | "completed";
id: string;
priority?: "high" | "medium" | "low";
}
interface TodoWriteArgs {
todos: TodoItem[];
}
interface TodoWriteResult {
message: string;
}
export async function todo_write(
args: TodoWriteArgs,
): Promise<TodoWriteResult> {
if (!args.todos || !Array.isArray(args.todos))
throw new Error("todos must be an array");
for (const todo of args.todos) {
if (!todo.content || typeof todo.content !== "string")
throw new Error("Each todo must have a content string");
if (
!todo.status ||
!["pending", "in_progress", "completed"].includes(todo.status)
)
throw new Error(
"Each todo must have a valid status (pending, in_progress, or completed)",
);
if (!todo.id || typeof todo.id !== "string")
throw new Error("Each todo must have an id string");
if (todo.priority && !["high", "medium", "low"].includes(todo.priority))
throw new Error("If provided, priority must be high, medium, or low");
}
return {
message:
"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable",
};
}

42
src/tools/impl/Write.ts Normal file
View File

@@ -0,0 +1,42 @@
import { promises as fs } from "node:fs";
import * as path from "node:path";
interface WriteArgs {
file_path: string;
content: string;
}
interface WriteResult {
message: string;
}
export async function write(args: WriteArgs): Promise<WriteResult> {
const { file_path, content } = args;
if (!path.isAbsolute(file_path))
throw new Error(`File path must be absolute, got: ${file_path}`);
try {
const dir = path.dirname(file_path);
await fs.mkdir(dir, { recursive: true });
try {
const stats = await fs.stat(file_path);
if (stats.isDirectory())
throw new Error(`Path is a directory, not a file: ${file_path}`);
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code !== "ENOENT") throw err;
}
await fs.writeFile(file_path, content, "utf-8");
return {
message: `Successfully wrote ${content.length} characters to ${file_path}`,
};
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code === "EACCES")
throw new Error(`Permission denied: ${file_path}`);
else if (err.code === "ENOSPC")
throw new Error(`No space left on device: ${file_path}`);
else if (err.code === "EISDIR")
throw new Error(`Path is a directory: ${file_path}`);
else if (err.message) throw err;
else throw new Error(`Failed to write file: ${err}`);
}
}

View File

@@ -0,0 +1,14 @@
export interface BackgroundProcess {
process: import("child_process").ChildProcess;
command: string;
stdout: string[];
stderr: string[];
status: "running" | "completed" | "failed";
exitCode: number | null;
lastReadIndex: { stdout: number; stderr: number };
startTime?: Date;
}
export const backgroundProcesses = new Map<string, BackgroundProcess>();
let bashIdCounter = 1;
export const getNextBashId = () => `bash_${bashIdCounter++}`;