1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5import { Type } from "@sinclair/typebox";
6import type { AgentTool } from "@mariozechner/pi-agent-core";
7import simpleGit from "simple-git";
8import { formatSize, truncateHead } from "../../../util/truncate.js";
9
10// Trust boundary: refs and paths are passed directly to simple-git, which is
11// scoped to the workspace. The user chose to clone this repo, so its contents
12// are trusted. See AGENTS.md § Workspace Sandboxing.
13
14const RefsSchema = Type.Object({
15 type: Type.Union([
16 Type.Literal("branches"),
17 Type.Literal("tags"),
18 Type.Literal("remotes"),
19 ]),
20});
21
22export const createGitRefsTool = (workspacePath: string): AgentTool => ({
23 name: "git_refs",
24 label: "Git Refs",
25 description: "List branches, tags, or remotes.",
26 parameters: RefsSchema as any,
27 execute: async (_toolCallId: string, params: any) => {
28 const git = simpleGit(workspacePath);
29
30 let raw: string;
31 let baseDetails: Record<string, any> = {};
32
33 if (params.type === "tags") {
34 const tags = await git.tags();
35 raw = tags.all.join("\n");
36 baseDetails = { count: tags.all.length };
37 } else if (params.type === "remotes") {
38 raw = await git.raw(["branch", "-r"]);
39 } else {
40 raw = await git.raw(["branch", "-a"]);
41 }
42
43 const truncation = truncateHead(raw);
44 let text = truncation.content;
45 if (truncation.truncated) {
46 text += `\n\n[truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`;
47 }
48
49 return {
50 content: [{ type: "text", text }],
51 details: { ...baseDetails, ...(truncation.truncated ? { truncation } : {}) },
52 };
53 },
54});