@anthropic-ai/sdk
Version:
The official TypeScript library for the Anthropic API
324 lines • 13.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BetaLocalFilesystemMemoryTool = exports.betaMemoryTool = void 0;
const tslib_1 = require("../../internal/tslib.js");
var memory_1 = require("../../helpers/beta/memory.js");
Object.defineProperty(exports, "betaMemoryTool", { enumerable: true, get: function () { return memory_1.betaMemoryTool; } });
const fs = tslib_1.__importStar(require("fs/promises"));
const path = tslib_1.__importStar(require("path"));
const crypto_1 = require("crypto");
async function exists(path) {
return await fs
.access(path)
.then(() => true)
.catch((err) => {
if (err.code === 'ENOENT')
return false;
throw err;
});
}
/**
* Atomically writes content to a file by writing to a temporary file first and then renaming it.
* This ensures the target file is never in a partially written state, preventing data corruption
* if the process crashes or is interrupted during the write operation. The rename operation is
* atomic on most file systems, guaranteeing that readers will only ever see the complete old
* content or the complete new content, never a mix or partial state.
*
* @param targetPath - The path where the file should be written
* @param content - The content to write to the file
*/
async function atomicWriteFile(targetPath, content) {
const dir = path.dirname(targetPath);
const tempPath = path.join(dir, `.tmp-${process.pid}-${(0, crypto_1.randomUUID)()}`);
let handle;
try {
handle = await fs.open(tempPath, 'wx');
await handle.writeFile(content, 'utf-8');
await handle.sync();
await handle.close();
handle = undefined;
await fs.rename(tempPath, targetPath);
}
catch (err) {
if (handle) {
await handle.close().catch(() => { });
}
await fs.unlink(tempPath).catch(() => { });
throw err;
}
}
/**
* Validates that a target path doesn't escape the memory root via symlinks.
*
* Prevents symlink attacks where a malicious symlink inside /memories points
* outside (e.g., /memories/foo -> /etc), which would allow operations like
* creating /memories/foo/passwd to actually write to /etc/passwd.
*
* Walks up from the target path to find the deepest existing ancestor,
* then resolves it to ensure the real path stays within memoryRoot.
*/
async function validateNoSymlinkEscape(targetPath, memoryRoot) {
const resolvedRoot = await fs.realpath(memoryRoot);
let current = targetPath;
while (true) {
try {
const resolved = await fs.realpath(current);
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + path.sep)) {
throw new Error(`Path would escape /memories directory via symlink`);
}
return;
}
catch (err) {
if (err.code !== 'ENOENT')
throw err;
const parent = path.dirname(current);
if (parent === current || current === memoryRoot) {
return;
}
current = parent;
}
}
}
async function readFileContent(fullPath, memoryPath) {
try {
return await fs.readFile(fullPath, 'utf-8');
}
catch (err) {
if (err.code === 'ENOENT') {
throw new Error(`The file ${memoryPath} no longer exists (may have been deleted or renamed concurrently).`);
}
throw err;
}
}
function formatFileSize(bytes) {
if (bytes === 0)
return '0B';
const k = 1024;
const sizes = ['B', 'K', 'M', 'G'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const size = bytes / Math.pow(k, i);
return (size % 1 === 0 ? size.toString() : size.toFixed(1)) + sizes[i];
}
const MAX_LINES = 999999;
const LINE_NUMBER_WIDTH = String(MAX_LINES).length;
class BetaLocalFilesystemMemoryTool {
constructor(basePath = './memory') {
this.basePath = basePath;
this.memoryRoot = path.join(this.basePath, 'memories');
}
static async init(basePath = './memory') {
const memory = new BetaLocalFilesystemMemoryTool(basePath);
await fs.mkdir(memory.memoryRoot, { recursive: true });
return memory;
}
async validatePath(memoryPath) {
if (!memoryPath.startsWith('/memories')) {
throw new Error(`Path must start with /memories, got: ${memoryPath}`);
}
const relativePath = memoryPath.slice('/memories'.length).replace(/^\//, '');
const fullPath = relativePath ? path.join(this.memoryRoot, relativePath) : this.memoryRoot;
const resolvedPath = path.resolve(fullPath);
const resolvedRoot = path.resolve(this.memoryRoot);
if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(resolvedRoot + path.sep)) {
throw new Error(`Path ${memoryPath} would escape /memories directory`);
}
await validateNoSymlinkEscape(resolvedPath, this.memoryRoot);
return resolvedPath;
}
async view(command) {
const fullPath = await this.validatePath(command.path);
let stat;
try {
stat = await fs.stat(fullPath);
}
catch (err) {
if (err.code === 'ENOENT') {
throw new Error(`The path ${command.path} does not exist. Please provide a valid path.`);
}
throw err;
}
if (stat.isDirectory()) {
const items = [];
const collectItems = async (dirPath, relativePath, depth) => {
if (depth > 2)
return;
const dirContents = await fs.readdir(dirPath);
for (const item of dirContents.sort()) {
if (item.startsWith('.') || item === 'node_modules') {
continue;
}
const itemPath = path.join(dirPath, item);
const itemRelativePath = relativePath ? `${relativePath}/${item}` : item;
let itemStat;
try {
itemStat = await fs.stat(itemPath);
}
catch {
continue;
}
if (itemStat.isDirectory()) {
items.push({ size: formatFileSize(itemStat.size), path: `${itemRelativePath}/` });
if (depth < 2) {
await collectItems(itemPath, itemRelativePath, depth + 1);
}
}
else if (itemStat.isFile()) {
items.push({ size: formatFileSize(itemStat.size), path: itemRelativePath });
}
}
};
await collectItems(fullPath, '', 1);
const header = `Here're the files and directories up to 2 levels deep in ${command.path}, excluding hidden items and node_modules:`;
const dirSize = formatFileSize(stat.size);
const lines = [
`${dirSize}\t${command.path}`,
...items.map((item) => `${item.size}\t${command.path}/${item.path}`),
];
return `${header}\n${lines.join('\n')}`;
}
else if (stat.isFile()) {
const content = await readFileContent(fullPath, command.path);
const lines = content.split('\n');
if (lines.length > MAX_LINES) {
throw new Error(`File ${command.path} has too many lines (${lines.length}). Maximum is ${MAX_LINES.toLocaleString()} lines.`);
}
let displayLines = lines;
let startNum = 1;
if (command.view_range && command.view_range.length === 2) {
const startLine = Math.max(1, command.view_range[0]) - 1;
const endLine = command.view_range[1] === -1 ? lines.length : command.view_range[1];
displayLines = lines.slice(startLine, endLine);
startNum = startLine + 1;
}
const numberedLines = displayLines.map((line, i) => `${String(i + startNum).padStart(LINE_NUMBER_WIDTH, ' ')}\t${line}`);
return `Here's the content of ${command.path} with line numbers:\n${numberedLines.join('\n')}`;
}
else {
throw new Error(`Unsupported file type for ${command.path}`);
}
}
async create(command) {
const fullPath = await this.validatePath(command.path);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
let handle;
try {
handle = await fs.open(fullPath, 'wx');
await handle.writeFile(command.file_text, 'utf-8');
await handle.sync();
}
catch (err) {
if (err?.code === 'EEXIST') {
throw new Error(`File ${command.path} already exists`);
}
throw err;
}
finally {
await handle?.close().catch(() => { });
}
return `File created successfully at: ${command.path}`;
}
async str_replace(command) {
const fullPath = await this.validatePath(command.path);
let stat;
try {
stat = await fs.stat(fullPath);
}
catch (err) {
if (err.code === 'ENOENT') {
throw new Error(`The path ${command.path} does not exist. Please provide a valid path.`);
}
throw err;
}
if (!stat.isFile()) {
throw new Error(`The path ${command.path} is not a file.`);
}
const content = await readFileContent(fullPath, command.path);
const lines = content.split('\n');
const matchingLines = [];
lines.forEach((line, index) => {
if (line.includes(command.old_str)) {
matchingLines.push(index + 1);
}
});
if (matchingLines.length === 0) {
throw new Error(`No replacement was performed, old_str \`${command.old_str}\` did not appear verbatim in ${command.path}.`);
}
else if (matchingLines.length > 1) {
throw new Error(`No replacement was performed. Multiple occurrences of old_str \`${command.old_str}\` in lines: ${matchingLines.join(', ')}. Please ensure it is unique`);
}
const newContent = content.replace(command.old_str, command.new_str);
await atomicWriteFile(fullPath, newContent);
const newLines = newContent.split('\n');
const changedLineIndex = matchingLines[0] - 1;
const contextStart = Math.max(0, changedLineIndex - 2);
const contextEnd = Math.min(newLines.length, changedLineIndex + 3);
const snippet = newLines.slice(contextStart, contextEnd).map((line, i) => {
const lineNum = contextStart + i + 1;
return `${String(lineNum).padStart(LINE_NUMBER_WIDTH, ' ')}\t${line}`;
});
return `The memory file has been edited. Here is the snippet showing the change (with line numbers):\n${snippet.join('\n')}`;
}
async insert(command) {
const fullPath = await this.validatePath(command.path);
let stat;
try {
stat = await fs.stat(fullPath);
}
catch (err) {
if (err.code === 'ENOENT') {
throw new Error(`The path ${command.path} does not exist. Please provide a valid path.`);
}
throw err;
}
if (!stat.isFile()) {
throw new Error(`The path ${command.path} is not a file.`);
}
const content = await readFileContent(fullPath, command.path);
const lines = content.split('\n');
if (command.insert_line < 0 || command.insert_line > lines.length) {
throw new Error(`Invalid \`insert_line\` parameter: ${command.insert_line}. It should be within the range of lines of the file: [0, ${lines.length}]`);
}
lines.splice(command.insert_line, 0, command.insert_text.replace(/\n$/, ''));
await atomicWriteFile(fullPath, lines.join('\n'));
return `The file ${command.path} has been edited.`;
}
async delete(command) {
const fullPath = await this.validatePath(command.path);
if (command.path === '/memories') {
throw new Error('Cannot delete the /memories directory itself');
}
try {
await fs.rm(fullPath, { recursive: true, force: false });
}
catch (err) {
if (err.code === 'ENOENT') {
throw new Error(`The path ${command.path} does not exist`);
}
throw err;
}
return `Successfully deleted ${command.path}`;
}
async rename(command) {
const oldFullPath = await this.validatePath(command.old_path);
const newFullPath = await this.validatePath(command.new_path);
// POSIX rename() silently overwrites existing files without error,
// so we can't catch this atomically. Best-effort check to warn user.
if (await exists(newFullPath)) {
throw new Error(`The destination ${command.new_path} already exists`);
}
const newDir = path.dirname(newFullPath);
await fs.mkdir(newDir, { recursive: true });
try {
await fs.rename(oldFullPath, newFullPath);
}
catch (err) {
if (err.code === 'ENOENT') {
throw new Error(`The path ${command.old_path} does not exist`);
}
throw err;
}
return `Successfully renamed ${command.old_path} to ${command.new_path}`;
}
}
exports.BetaLocalFilesystemMemoryTool = BetaLocalFilesystemMemoryTool;
//# sourceMappingURL=node.js.map