src / tools / gitTools.ts

/**
 * @file gitTools.ts
 * Git operations: status, diff, commit, log.
 */

import { tool, type Tool } from "@lmstudio/sdk";
import { z } from "zod";
import { validatePath, type ToolContext } from "./shared";

export function createGitTools(ctx: ToolContext): Tool[] {
  const gitStatusTool = tool({
    name: "git_status",
    description: "Get the current git status of the repository.",
    parameters: {},
    implementation: async () => {
      const { simpleGit } = await import("simple-git");
      const git = simpleGit(ctx.cwd);
      try {
        return await git.status();
      } catch (e) {
        return { error: `Git status failed: ${e instanceof Error ? e.message : String(e)}` };
      }
    },
  });

  const gitDiffTool = tool({
    name: "git_diff",
    description: "Get the git diff of the current repository or specific files.",
    parameters: {
      file_path: z.string().optional().describe("Optional: Path to specific file to diff."),
      cached: z.boolean().optional().describe("Optional: Show staged changes only (git diff --cached)."),
    },
    implementation: async ({ file_path, cached }) => {
      const { simpleGit } = await import("simple-git");
      const git = simpleGit(ctx.cwd);
      try {
        const args: string[] = [];
        if (cached) args.push("--cached");
        if (file_path) args.push(validatePath(ctx.cwd, file_path));
        const MAX_DIFF_CHARS = 8000;
        const rawDiff = await git.diff(args);
        if (!rawDiff) return { diff: "No changes." };
        const truncated = rawDiff.length > MAX_DIFF_CHARS;
        return {
          diff: rawDiff.substring(0, MAX_DIFF_CHARS) + (truncated ? `\n... (truncated, showing ${MAX_DIFF_CHARS} of ${rawDiff.length} chars)` : ""),
          ...(truncated ? { warning: "Diff was truncated. Use file_path to diff a specific file." } : {}),
        };
      } catch (e) {
        return { error: `Git diff failed: ${e instanceof Error ? e.message : String(e)}` };
      }
    },
  });

  const gitCommitTool = tool({
    name: "git_commit",
    description: "Commit staged changes to the git repository.",
    parameters: { message: z.string() },
    implementation: async ({ message }) => {
      const { simpleGit } = await import("simple-git");
      const git = simpleGit(ctx.cwd);
      try {
        const result = await git.commit(message);
        return { success: true, summary: result.summary };
      } catch (e) {
        return { error: `Git commit failed: ${e instanceof Error ? e.message : String(e)}` };
      }
    },
  });

  const gitLogTool = tool({
    name: "git_log",
    description: "Get recent git commit history.",
    parameters: {
      max_count: z.number().optional().describe("Max number of commits to return (default: 10)"),
    },
    implementation: async ({ max_count = 10 }) => {
      const { simpleGit } = await import("simple-git");
      const git = simpleGit(ctx.cwd);
      try {
        const log = await git.log({ maxCount: max_count });
        return { history: log.all };
      } catch (e) {
        return { error: `Git log failed: ${e instanceof Error ? e.message : String(e)}` };
      }
    },
  });

  return [gitStatusTool, gitDiffTool, gitCommitTool, gitLogTool];
}