log.ts

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