diff.ts

 1import { Type } from "@sinclair/typebox";
 2import type { AgentTool } from "@mariozechner/pi-ai";
 3import simpleGit from "simple-git";
 4import { ToolInputError } from "../../../util/errors.js";
 5import { formatSize, truncateHead } from "../../../util/truncate.js";
 6
 7const DiffSchema = Type.Object({
 8  ref: Type.Optional(Type.String({ description: "Base ref (optional)" })),
 9  ref2: Type.Optional(Type.String({ description: "Compare ref (optional)" })),
10  path: Type.Optional(Type.String({ description: "Limit diff to path" })),
11});
12
13export const createGitDiffTool = (workspacePath: string): AgentTool => ({
14  name: "git_diff",
15  label: "Git Diff",
16  description: "Show diff between refs or working tree.",
17  parameters: DiffSchema as any,
18  execute: async (_toolCallId: string, params: any) => {
19    const git = simpleGit(workspacePath);
20    const args: string[] = [];
21
22    if (params.ref && !String(params.ref).trim()) {
23      throw new ToolInputError("ref must be a non-empty string");
24    }
25    if (params.ref2 && !String(params.ref2).trim()) {
26      throw new ToolInputError("ref2 must be a non-empty string");
27    }
28    if (params.path && !String(params.path).trim()) {
29      throw new ToolInputError("path must be a non-empty string");
30    }
31    if (params.ref) args.push(params.ref);
32    if (params.ref2) args.push(params.ref2);
33    if (params.path) args.push("--", params.path);
34
35    const raw = await git.diff(args);
36    const truncation = truncateHead(raw);
37
38    let text = truncation.content;
39    if (truncation.truncated) {
40      text += `\n\n[truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`;
41    }
42
43    return {
44      content: [{ type: "text", text }],
45      details: { path: params.path ?? null, ...(truncation.truncated ? { truncation } : {}) },
46    };
47  },
48});