1import { Type } from "@sinclair/typebox";
2import type { AgentTool } from "@mariozechner/pi-ai";
3import simpleGit from "simple-git";
4import { ToolInputError } from "../../../util/errors.js";
5
6const LogSchema = Type.Object({
7 path: Type.Optional(Type.String({ description: "Filter to commits touching this path" })),
8 author: Type.Optional(Type.String({ description: "Filter by author name/email" })),
9 since: Type.Optional(Type.String({ description: "Commits after this date (e.g., 2024-01-01)" })),
10 until: Type.Optional(Type.String({ description: "Commits before this date" })),
11 n: Type.Optional(Type.Number({ description: "Maximum number of commits (default: 20)" })),
12 oneline: Type.Optional(Type.Boolean({ description: "Compact one-line format (default: false)" })),
13});
14
15export const createGitLogTool = (workspacePath: string): AgentTool => ({
16 name: "git_log",
17 label: "Git Log",
18 description: "View commit history. Supports filtering by path, author, date range, and count.",
19 parameters: LogSchema as any,
20 execute: async (_toolCallId: string, params: any) => {
21 const git = simpleGit(workspacePath);
22 const options: string[] = [];
23
24 if (params.n !== undefined) {
25 if (typeof params.n !== "number" || Number.isNaN(params.n) || params.n <= 0) {
26 throw new ToolInputError("n must be a positive number");
27 }
28 options.push("-n", String(Math.floor(params.n)));
29 }
30 if (params.oneline) options.push("--oneline");
31 if (params.author && !String(params.author).trim()) {
32 throw new ToolInputError("author must be a non-empty string");
33 }
34 if (params.author) options.push(`--author=${params.author}`);
35 if (params.since && !String(params.since).trim()) {
36 throw new ToolInputError("since must be a non-empty string");
37 }
38 if (params.since) options.push(`--since=${params.since}`);
39 if (params.until && !String(params.until).trim()) {
40 throw new ToolInputError("until must be a non-empty string");
41 }
42 if (params.until) options.push(`--until=${params.until}`);
43
44 const result = await git.log(options.concat(params.path ? ["--", params.path] : []));
45
46 const text = result.all
47 .map((entry) =>
48 params.oneline
49 ? `${entry.hash} ${entry.message}`
50 : `${entry.hash} ${entry.date} ${entry.author_name} ${entry.message}`,
51 )
52 .join("\n");
53
54 return {
55 content: [{ type: "text", text }],
56 details: { count: result.all.length },
57 };
58 },
59});