Project Files
src / tools / git-ls-files-tool.ts
import { tool } from "@lmstudio/sdk"
import { simpleGit } from "simple-git"
import { z } from "zod"
import { formatGitError } from "../git/error-formatting"
import { gitSafeString } from "../git/parameter-schemas"
import { resolveAndValidateRepoPath } from "../path/validation"
import type { Tool, ToolsProviderController } from "@lmstudio/sdk"
/**
* Maximum size in bytes of the raw `git ls-files` output to load into memory.
*
* @const {number}
* @default 10485760
*/
const maxOutputBytes = 10 * 1024 * 1024
/**
* Maximum number of paths a single paginated listing can return.
*
* @const {number}
* @default 10000
*/
const maxFileLimit = 10_000
/**
* Maximum allowed value for the offset parameter.
*
* @const {number}
* @default 10000000
*/
const maxOffset = 10_000_000
/**
* Maximum allowed character length for path strings (repoName and pathspec).
*
* @const {number}
* @default 4096
*/
const maxFilePathLength = 4096
/**
* Create the git ls-files tool.
*
* @param ctl Tools provider controller supplied by the LM Studio SDK.
* @returns The configured git ls-files tool.
*/
export function createGitLsFilesTool(ctl: ToolsProviderController): Tool {
return tool({
name: "git_ls_files",
description:
"List tracked files in a cloned git repository as a recursive flat list. Use this to discover the repository structure and list or locate tracked files.",
parameters: {
repoName: gitSafeString("repoName", maxFilePathLength).describe("Directory name of the cloned git repository."),
pathspec: gitSafeString("pathspec", maxFilePathLength)
.optional()
.describe(
"Optional git pathspec pattern to filter the file listing relative to the repository root. Valid pathspecs include exact filenames, directory paths, or a wildcard pattern. Omit the pathspec to list all tracked files in the repository."
),
offset: z
.number()
.int()
.min(0)
.max(maxOffset)
.optional()
.describe("0-based index of the first file to return for pagination. Omit to start from the first file."),
limit: z
.number()
.int()
.min(1)
.max(maxFileLimit)
.optional()
.describe(`Maximum number of files to return (max ${maxFileLimit}). Omit to return all matching files.`),
},
/**
* Executes `git ls-files`, optionally filtered by pathspec, and returns a paginated JSON listing.
*
* @param arguments_ Validated tool parameters.
* @param arguments_.repoName Repository directory name relative to the working directory.
* @param arguments_.pathspec Optional git pathspec narrowing the listing.
* @param arguments_.offset 0-based index to start reading from in the file list.
* @param arguments_.limit Maximum number of files to return.
* @param context Runtime tool context supplied by the SDK.
* @returns The tracked-file listing as JSON, or a clear error string.
*/
implementation: async (arguments_, context) => {
const { repoName, pathspec, offset, limit } = arguments_
const repoPath = resolveAndValidateRepoPath(repoName, ctl.getWorkingDirectory())
const git = simpleGit(repoPath)
const target = typeof pathspec === "string" ? `"${repoName}" matching "${pathspec}"` : `"${repoName}"`
context.status(`Listing tracked files in ${target}…`)
try {
const raw =
typeof pathspec === "string" ? await git.raw(["ls-files", "--", pathspec]) : await git.raw(["ls-files"])
const outputBytes = Buffer.byteLength(raw, "utf8")
if (outputBytes > maxOutputBytes) {
return `Error: git_ls_files output for "${repoName}" exceeds the maximum allowed size of ${maxOutputBytes} bytes (${outputBytes} bytes). Narrow the listing with a more specific pathspec.`
}
const files = raw.split("\n").filter(line => line.length > 0)
const totalCount = files.length
const start = offset === undefined ? 0 : Math.min(offset, totalCount)
if (limit === undefined) {
const sliced = files.slice(start)
return JSON.stringify(
{
files: sliced,
total_count: totalCount,
from: start + 1,
to: totalCount,
},
undefined,
2
)
}
const end = Math.min(start + limit, totalCount)
const sliced = files.slice(start, end)
return JSON.stringify(
{
files: sliced,
total_count: totalCount,
from: start + 1,
to: end,
},
undefined,
2
)
} catch (error) {
return formatGitError("git_ls_files", repoName, error)
}
},
})
}