@earendil-works/pi-coding-agent
Version:
Coding agent CLI with read, bash, edit, write tools and session management
127 lines • 6.42 kB
JavaScript
import { readdir as fsReaddir, stat as fsStat } from "node:fs/promises";
import nodePath from "path";
import { Type } from "typebox";
import { pathExists, resolveToCwd } from "./path-utils.js";
import { lsRenderers } from "./renderers/ls.js";
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
import { DEFAULT_MAX_BYTES, formatSize, truncateHead } from "./truncate.js";
const lsSchema = Type.Object({
path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })),
});
export const lsToolSystemPromptContribution = {
snippet: "List directory contents",
guidelines: [],
};
const DEFAULT_LIMIT = 500;
const defaultLsOperations = {
exists: pathExists,
stat: fsStat,
readdir: fsReaddir,
};
export function createLsToolDefinition(cwd, options) {
const ops = options?.operations ?? defaultLsOperations;
return {
name: "ls",
label: "ls",
description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
promptSnippet: lsToolSystemPromptContribution.snippet,
parameters: lsSchema,
async execute(_toolCallId, { path, limit }, signal, _onUpdate, ctx) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(new Error("Operation aborted"));
return;
}
const onAbort = () => reject(new Error("Operation aborted"));
signal?.addEventListener("abort", onAbort, { once: true });
(async () => {
try {
const dirPath = resolveToCwd(path || ".", ctx?.cwd || cwd);
// A limit of 0 would list nothing and report "(empty directory)", so it means 1.
const effectiveLimit = Math.max(1, Math.floor(limit ?? DEFAULT_LIMIT));
// Check if path exists.
if (!(await ops.exists(dirPath))) {
reject(new Error(`Path not found: ${dirPath}`));
return;
}
// Check if path is a directory.
const stat = await ops.stat(dirPath);
if (!stat.isDirectory()) {
reject(new Error(`Not a directory: ${dirPath}`));
return;
}
// Read directory entries.
let entries;
try {
entries = await ops.readdir(dirPath);
}
catch (e) {
reject(new Error(`Cannot read directory: ${e.message}`));
return;
}
// Sort alphabetically, case-insensitive.
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
// Format entries with directory indicators.
const results = [];
let entryLimitReached = false;
for (const entry of entries) {
if (results.length >= effectiveLimit) {
entryLimitReached = true;
break;
}
const fullPath = nodePath.join(dirPath, entry);
let suffix = "";
try {
const entryStat = await ops.stat(fullPath);
if (entryStat.isDirectory())
suffix = "/";
}
catch {
// Skip entries we cannot stat.
continue;
}
results.push(entry + suffix);
}
signal?.removeEventListener("abort", onAbort);
if (results.length === 0) {
resolve({ content: [{ type: "text", text: "(empty directory)" }], details: undefined });
return;
}
const rawOutput = results.join("\n");
// Apply byte truncation. There is no separate line limit because entry count is already capped.
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
let output = truncation.content;
const details = {};
// Build actionable notices for truncation and entry limits.
const notices = [];
if (entryLimitReached) {
notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
details.entryLimitReached = effectiveLimit;
}
if (truncation.truncated) {
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
details.truncation = truncation;
}
if (notices.length > 0) {
output += `\n\n[${notices.join(". ")}]`;
}
resolve({
content: [{ type: "text", text: output }],
details: Object.keys(details).length > 0 ? details : undefined,
});
}
catch (e) {
signal?.removeEventListener("abort", onAbort);
reject(e);
}
})();
});
},
...lsRenderers,
};
}
export function createLsTool(cwd, options) {
return wrapToolDefinition(createLsToolDefinition(cwd, options));
}
//# sourceMappingURL=ls.js.map