openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
158 lines (157 loc) • 5.67 kB
JavaScript
import { c as extractArchive, n as ArchiveLimitError, t as ARCHIVE_LIMIT_ERROR_CODE } from "./archive-R2ClviiB.mjs";
import { n as textResult } from "./tool-results-BCM3fdVS.mjs";
import { f as saveMediaBuffer } from "./store-7jnF8fA5.mjs";
import "./archive-D3lhZQjN.mjs";
import "./media-store-BbouPwWH.mjs";
import { a as FILE_TRANSFER_SUBDIR, t as DIR_FETCH_TOOL_DESCRIPTOR } from "./descriptors-BNnHhU6g.mjs";
import { i as DIR_FETCH_HARD_MAX_BYTES, r as DIR_FETCH_DEFAULT_MAX_BYTES, t as DIR_FETCH_ARCHIVE_POLICY } from "./dir-fetch-archive-WQvCNXYk.mjs";
import { r as mimeFromExtension, t as IMAGE_MIME_INLINE_SET } from "./mime-YhVRnsMm.mjs";
import { i as readClampedInt, n as readRequiredNodePath, t as invokeNodeToolPayload } from "./node-tool-invoke-DAwwwonl.mjs";
import { t as renderDirectoryText } from "./directory-text-DECpN6xv.mjs";
import { t as decodeFetchPayload } from "./fetch-payload-mhJgiC4_.mjs";
import path from "node:path";
import fs from "node:fs/promises";
import { walkDirectory } from "@openclaw/fs-safe/walk";
import { sha256File } from "@openclaw/fs-safe/durability";
//#region extensions/file-transfer/src/tools/dir-fetch-tool.ts
const MEDIA_URL_CAP = 25;
const TAR_UNPACK_TIMEOUT_MS = 6e4;
function classifyArchiveFailure(error) {
const reason = error instanceof Error ? error.message : String(error);
if (error instanceof ArchiveLimitError && error.code !== ARCHIVE_LIMIT_ERROR_CODE.ENTRY_COUNT_EXCEEDS_LIMIT) return {
auditCode: "TREE_TOO_LARGE",
publicCode: "UNCOMPRESSED_TOO_LARGE",
reason
};
return {
auditCode: "UNSAFE_ARCHIVE",
publicCode: "UNSAFE_ARCHIVE",
reason
};
}
function savedDirectoryText(rootDir, files) {
const header = JSON.stringify({
rootDir,
fileCount: files.length
}).slice(0, -1);
return renderDirectoryText({
entries: files.toSorted((a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0),
project: ({ relPath, size }) => ({
relPath,
size
}),
render: (visible) => {
const manifest = `${header},"displayedCount":${visible.length},"files":[${visible.join(",")}]}`;
const note = `${files.length - visible.length} saved files omitted from this text (byte limit or reserved path markers). All remain under rootDir; inspect them with available local file or directory capabilities.`;
return {
manifest,
text: `Fetched ${files.length} files.\n${manifest}\n${note}`
};
},
fallback: `Fetched ${files.length} files. Saved paths omitted: rootDir cannot be represented safely within the 8192-byte text limit. No usable local path is shown.`
});
}
function createDirFetchTool() {
return {
...DIR_FETCH_TOOL_DESCRIPTOR,
execute: async (_toolCallId, args) => {
const params = args;
const { node, requestedPath: dirPath } = readRequiredNodePath(params);
const maxBytes = readClampedInt({
input: params,
key: "maxBytes",
defaultValue: DIR_FETCH_DEFAULT_MAX_BYTES,
hardMax: DIR_FETCH_HARD_MAX_BYTES
});
const { audit, payload } = await invokeNodeToolPayload({
node,
params,
command: "dir.fetch",
commandParams: {
path: dirPath,
maxBytes
},
requestedPath: dirPath
});
const { canonicalPath, size: tarBytes, sha256, buffer: tarBuffer } = decodeFetchPayload("dir.fetch", payload);
const savedTar = await saveMediaBuffer(tarBuffer, "application/gzip", FILE_TRANSFER_SUBDIR, DIR_FETCH_HARD_MAX_BYTES);
const tarDir = path.dirname(savedTar.path);
const unpackId = `dir-fetch-${path.basename(savedTar.path, path.extname(savedTar.path))}`;
const rootDir = path.join(tarDir, unpackId);
await fs.mkdir(rootDir, {
recursive: true,
mode: 448
});
try {
await extractArchive({
archivePath: savedTar.path,
destDir: rootDir,
kind: "tar",
tarGzip: true,
timeoutMs: TAR_UNPACK_TIMEOUT_MS,
entryModes: "clamp",
...DIR_FETCH_ARCHIVE_POLICY
});
} catch (error) {
await Promise.all([fs.rm(rootDir, {
recursive: true,
force: true
}).catch(() => void 0), fs.rm(savedTar.path, { force: true }).catch(() => void 0)]);
const failure = classifyArchiveFailure(error);
await audit({
canonicalPath,
decision: "error",
errorCode: failure.auditCode,
errorMessage: failure.reason,
sizeBytes: tarBytes,
sha256
});
throw new Error(`dir.fetch ${failure.publicCode}: ${failure.reason}`, { cause: error });
}
const walked = await walkDirectory(rootDir, {
symlinks: "skip",
include: ({ kind }) => kind === "file"
});
if (walked.failedDirs.length > 0) throw walked.failedDirs[0].error;
const files = [];
for (const { relativePath: relPath, path: absPath } of walked.entries) {
let size;
try {
size = (await fs.stat(absPath)).size;
} catch {
continue;
}
const mimeType = mimeFromExtension(relPath);
const fileSha256 = (await sha256File(absPath)).digest;
files.push({
relPath,
size,
mimeType,
sha256: fileSha256,
localPath: absPath
});
}
const fileCount = files.length;
const imageFiles = files.filter((f) => IMAGE_MIME_INLINE_SET.has(f.mimeType));
const nonImageFiles = files.filter((f) => !IMAGE_MIME_INLINE_SET.has(f.mimeType));
const mediaUrls = [...imageFiles, ...nonImageFiles].slice(0, MEDIA_URL_CAP).map((f) => f.localPath);
await audit({
canonicalPath,
decision: "allowed",
sizeBytes: tarBytes,
sha256
});
return textResult(savedDirectoryText(rootDir, files), {
path: canonicalPath,
rootDir,
fileCount,
tarBytes,
sha256,
files,
media: { mediaUrls }
});
}
};
}
//#endregion
export { createDirFetchTool };