2025-10-26 19:50:41 +00:00
|
|
|
import z from "zod"
|
2025-05-31 18:41:00 +00:00
|
|
|
import { Tool } from "./tool"
|
|
|
|
|
import * as path from "path"
|
2025-06-04 17:12:13 +00:00
|
|
|
import DESCRIPTION from "./ls.txt"
|
2025-09-01 21:15:49 +00:00
|
|
|
import { Instance } from "../project/instance"
|
2025-09-02 15:40:20 +00:00
|
|
|
import { Ripgrep } from "../file/ripgrep"
|
2026-01-10 23:49:36 +00:00
|
|
|
import { assertExternalDirectory } from "./external-directory"
|
2025-05-26 17:44:59 +00:00
|
|
|
|
2025-06-16 00:24:51 +00:00
|
|
|
export const IGNORE_PATTERNS = [
|
2025-05-30 20:39:45 +00:00
|
|
|
"node_modules/",
|
|
|
|
|
"__pycache__/",
|
|
|
|
|
".git/",
|
|
|
|
|
"dist/",
|
|
|
|
|
"build/",
|
|
|
|
|
"target/",
|
|
|
|
|
"vendor/",
|
|
|
|
|
"bin/",
|
|
|
|
|
"obj/",
|
|
|
|
|
".idea/",
|
|
|
|
|
".vscode/",
|
2025-07-09 15:35:06 +00:00
|
|
|
".zig-cache/",
|
|
|
|
|
"zig-out",
|
2025-07-12 16:06:58 +00:00
|
|
|
".coverage",
|
|
|
|
|
"coverage/",
|
|
|
|
|
"vendor/",
|
|
|
|
|
"tmp/",
|
|
|
|
|
"temp/",
|
|
|
|
|
".cache/",
|
|
|
|
|
"cache/",
|
|
|
|
|
"logs/",
|
|
|
|
|
".venv/",
|
|
|
|
|
"venv/",
|
|
|
|
|
"env/",
|
2025-05-31 18:41:00 +00:00
|
|
|
]
|
2025-05-26 17:44:59 +00:00
|
|
|
|
2025-06-16 00:24:51 +00:00
|
|
|
const LIMIT = 100
|
|
|
|
|
|
2025-07-25 17:29:29 +00:00
|
|
|
export const ListTool = Tool.define("list", {
|
2025-06-04 17:12:13 +00:00
|
|
|
description: DESCRIPTION,
|
2025-05-26 17:44:59 +00:00
|
|
|
parameters: z.object({
|
2025-07-07 19:53:43 +00:00
|
|
|
path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
|
|
|
|
|
ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
|
2025-05-26 17:44:59 +00:00
|
|
|
}),
|
2026-01-01 22:54:11 +00:00
|
|
|
async execute(params, ctx) {
|
2025-09-01 21:15:49 +00:00
|
|
|
const searchPath = path.resolve(Instance.directory, params.path || ".")
|
2026-01-10 23:49:36 +00:00
|
|
|
await assertExternalDirectory(ctx, searchPath, { kind: "directory" })
|
2025-05-26 17:44:59 +00:00
|
|
|
|
2026-01-01 22:54:11 +00:00
|
|
|
await ctx.ask({
|
|
|
|
|
permission: "list",
|
|
|
|
|
patterns: [searchPath],
|
|
|
|
|
always: ["*"],
|
|
|
|
|
metadata: {
|
|
|
|
|
path: searchPath,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2025-09-02 15:40:20 +00:00
|
|
|
const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(params.ignore?.map((p) => `!${p}`) || [])
|
2025-10-01 07:37:01 +00:00
|
|
|
const files = []
|
|
|
|
|
for await (const file of Ripgrep.files({ cwd: searchPath, glob: ignoreGlobs })) {
|
|
|
|
|
files.push(file)
|
|
|
|
|
if (files.length >= LIMIT) break
|
|
|
|
|
}
|
2025-05-26 17:44:59 +00:00
|
|
|
|
2025-05-30 20:39:45 +00:00
|
|
|
// Build directory structure
|
2025-05-31 18:41:00 +00:00
|
|
|
const dirs = new Set<string>()
|
|
|
|
|
const filesByDir = new Map<string, string[]>()
|
2025-05-26 17:44:59 +00:00
|
|
|
|
2025-05-30 20:39:45 +00:00
|
|
|
for (const file of files) {
|
2025-05-31 18:41:00 +00:00
|
|
|
const dir = path.dirname(file)
|
|
|
|
|
const parts = dir === "." ? [] : dir.split("/")
|
2025-05-26 17:44:59 +00:00
|
|
|
|
2025-05-30 20:39:45 +00:00
|
|
|
// Add all parent directories
|
|
|
|
|
for (let i = 0; i <= parts.length; i++) {
|
2025-05-31 18:41:00 +00:00
|
|
|
const dirPath = i === 0 ? "." : parts.slice(0, i).join("/")
|
|
|
|
|
dirs.add(dirPath)
|
2025-05-26 17:44:59 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-30 20:39:45 +00:00
|
|
|
// Add file to its directory
|
2025-05-31 18:41:00 +00:00
|
|
|
if (!filesByDir.has(dir)) filesByDir.set(dir, [])
|
|
|
|
|
filesByDir.get(dir)!.push(path.basename(file))
|
2025-05-26 17:44:59 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-30 20:39:45 +00:00
|
|
|
function renderDir(dirPath: string, depth: number): string {
|
2025-05-31 18:41:00 +00:00
|
|
|
const indent = " ".repeat(depth)
|
|
|
|
|
let output = ""
|
2025-05-26 17:44:59 +00:00
|
|
|
|
2025-05-30 20:39:45 +00:00
|
|
|
if (depth > 0) {
|
2025-05-31 18:41:00 +00:00
|
|
|
output += `${indent}${path.basename(dirPath)}/\n`
|
2025-05-26 17:44:59 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-31 18:41:00 +00:00
|
|
|
const childIndent = " ".repeat(depth + 1)
|
2025-05-30 20:39:45 +00:00
|
|
|
const children = Array.from(dirs)
|
|
|
|
|
.filter((d) => path.dirname(d) === dirPath && d !== dirPath)
|
2025-05-31 18:41:00 +00:00
|
|
|
.sort()
|
2025-05-26 17:44:59 +00:00
|
|
|
|
2025-05-30 20:39:45 +00:00
|
|
|
// Render subdirectories first
|
|
|
|
|
for (const child of children) {
|
2025-05-31 18:41:00 +00:00
|
|
|
output += renderDir(child, depth + 1)
|
2025-05-30 20:39:45 +00:00
|
|
|
}
|
2025-05-26 17:44:59 +00:00
|
|
|
|
2025-05-30 20:39:45 +00:00
|
|
|
// Render files
|
2025-05-31 18:41:00 +00:00
|
|
|
const files = filesByDir.get(dirPath) || []
|
2025-05-30 20:39:45 +00:00
|
|
|
for (const file of files.sort()) {
|
2025-05-31 18:41:00 +00:00
|
|
|
output += `${childIndent}${file}\n`
|
2025-05-26 17:44:59 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-31 18:41:00 +00:00
|
|
|
return output
|
2025-05-26 17:44:59 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-31 18:41:00 +00:00
|
|
|
const output = `${searchPath}/\n` + renderDir(".", 0)
|
2025-05-26 17:44:59 +00:00
|
|
|
|
2025-05-30 20:39:45 +00:00
|
|
|
return {
|
2025-09-01 21:15:49 +00:00
|
|
|
title: path.relative(Instance.worktree, searchPath),
|
2025-06-11 16:58:06 +00:00
|
|
|
metadata: {
|
|
|
|
|
count: files.length,
|
2025-06-16 00:24:51 +00:00
|
|
|
truncated: files.length >= LIMIT,
|
2025-06-11 16:58:06 +00:00
|
|
|
},
|
2025-05-30 20:39:45 +00:00
|
|
|
output,
|
2025-05-31 18:41:00 +00:00
|
|
|
}
|
2025-05-30 20:39:45 +00:00
|
|
|
},
|
2025-05-31 18:41:00 +00:00
|
|
|
})
|