package tools import ( "context" "encoding/json" "fmt" "git.gobha.me/xcaliber/chat-switchboard/workspace" ) // RegisterGitTools registers the 5 git-related workspace tools. // Only meaningful when a workspace has git_remote_url set — the completion // handler filters them out otherwise (via GitToolNames). func RegisterGitTools(gitOps *workspace.GitOps) { Register(&gitStatusTool{gitOps: gitOps}) Register(&gitDiffTool{gitOps: gitOps}) Register(&gitCommitTool{gitOps: gitOps}) Register(&gitLogTool{gitOps: gitOps}) Register(&gitBranchTool{gitOps: gitOps}) } // GitToolNames returns the names of all git tools for filtering. func GitToolNames() []string { return []string{"git_status", "git_diff", "git_commit", "git_log", "git_branch"} } // ── git_status ─────────────────────────────── type gitStatusTool struct{ gitOps *workspace.GitOps } func (t *gitStatusTool) Definition() ToolDef { return ToolDef{ Name: "git_status", DisplayName: "Git Status", Description: "Show git working tree status: branch name, staged/modified/untracked files, ahead/behind remote counts.", Category: "workspace", Parameters: JSONSchema(map[string]interface{}{}, nil), } } func (t *gitStatusTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) { w, err := t.gitOps.LoadWorkspace(ctx, execCtx.WorkspaceID) if err != nil { return "", err } status, err := t.gitOps.Status(ctx, w) if err != nil { return "", err } data, _ := json.MarshalIndent(status, "", " ") return string(data), nil } // ── git_diff ───────────────────────────────── type gitDiffTool struct{ gitOps *workspace.GitOps } func (t *gitDiffTool) Definition() ToolDef { return ToolDef{ Name: "git_diff", DisplayName: "Git Diff", Description: "Show git diff for a specific file or all uncommitted changes.", Category: "workspace", Parameters: JSONSchema(map[string]interface{}{ "path": Prop("string", "File path to diff (empty for all changes)"), }, nil), } } func (t *gitDiffTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) { var args struct { Path string `json:"path"` } json.Unmarshal([]byte(argsJSON), &args) w, err := t.gitOps.LoadWorkspace(ctx, execCtx.WorkspaceID) if err != nil { return "", err } diff, err := t.gitOps.Diff(ctx, w, args.Path) if err != nil { return "", err } if diff == "" { return "No changes.", nil } if len(diff) > 50000 { diff = diff[:50000] + "\n... (truncated)" } return diff, nil } // ── git_commit ─────────────────────────────── type gitCommitTool struct{ gitOps *workspace.GitOps } func (t *gitCommitTool) Definition() ToolDef { return ToolDef{ Name: "git_commit", DisplayName: "Git Commit", Description: "Stage and commit files. Stages all changes if no paths specified.", Category: "workspace", Parameters: JSONSchema(map[string]interface{}{ "message": Prop("string", "Commit message"), "paths": PropArray("Specific file paths to stage (empty = stage all)"), }, []string{"message"}), } } func (t *gitCommitTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) { var args struct { Message string `json:"message"` Paths []string `json:"paths"` } if err := json.Unmarshal([]byte(argsJSON), &args); err != nil { return "", fmt.Errorf("invalid arguments: %w", err) } if args.Message == "" { return "", fmt.Errorf("commit message is required") } w, err := t.gitOps.LoadWorkspace(ctx, execCtx.WorkspaceID) if err != nil { return "", err } if err := t.gitOps.Commit(ctx, w, args.Message, args.Paths); err != nil { return "", err } return fmt.Sprintf("Committed: %s", args.Message), nil } // ── git_log ────────────────────────────────── type gitLogTool struct{ gitOps *workspace.GitOps } func (t *gitLogTool) Definition() ToolDef { return ToolDef{ Name: "git_log", DisplayName: "Git Log", Description: "Show recent commit history.", Category: "workspace", Parameters: JSONSchema(map[string]interface{}{ "count": Prop("integer", "Number of commits to show (default 20, max 100)"), }, nil), } } func (t *gitLogTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) { var args struct { Count int `json:"count"` } json.Unmarshal([]byte(argsJSON), &args) if args.Count <= 0 { args.Count = 20 } if args.Count > 100 { args.Count = 100 } w, err := t.gitOps.LoadWorkspace(ctx, execCtx.WorkspaceID) if err != nil { return "", err } entries, err := t.gitOps.Log(ctx, w, args.Count) if err != nil { return "", err } data, _ := json.MarshalIndent(entries, "", " ") return string(data), nil } // ── git_branch ─────────────────────────────── type gitBranchTool struct{ gitOps *workspace.GitOps } func (t *gitBranchTool) Definition() ToolDef { return ToolDef{ Name: "git_branch", DisplayName: "Git Branch", Description: "List branches or switch to a different branch.", Category: "workspace", Parameters: JSONSchema(map[string]interface{}{ "checkout": Prop("string", "Branch name to switch to (empty = list only)"), }, nil), } } func (t *gitBranchTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) { var args struct { Checkout string `json:"checkout"` } json.Unmarshal([]byte(argsJSON), &args) w, err := t.gitOps.LoadWorkspace(ctx, execCtx.WorkspaceID) if err != nil { return "", err } if args.Checkout != "" { if err := t.gitOps.Checkout(ctx, w, args.Checkout); err != nil { return "", err } return fmt.Sprintf("Switched to branch: %s", args.Checkout), nil } branches, current, err := t.gitOps.BranchList(ctx, w) if err != nil { return "", err } result := map[string]interface{}{ "current": current, "branches": branches, } data, _ := json.MarshalIndent(result, "", " ") return string(data), nil }