diff --git a/Utilities/GitUtility.cs b/Utilities/GitUtility.cs
new file mode 100644
index 0000000..4c4cd2e
--- /dev/null
+++ b/Utilities/GitUtility.cs
@@ -0,0 +1,180 @@
+namespace CodeMedic.Utilities;
+
+///
+/// Utility class for Git-related operations.
+///
+public static class GitUtility
+{
+ ///
+ /// Checks if the specified directory is a Git repository.
+ ///
+ /// The path to check.
+ /// True if the path is within a Git repository; otherwise, false.
+ public static bool IsGitRepository(string path)
+ {
+ var directory = new DirectoryInfo(path);
+
+ while (directory != null)
+ {
+ var gitDir = Path.Combine(directory.FullName, ".git");
+ if (Directory.Exists(gitDir) || File.Exists(gitDir))
+ {
+ return true;
+ }
+ directory = directory.Parent;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Gets the root directory of the Git repository.
+ ///
+ /// A path within the repository.
+ /// The repository root path, or null if not in a Git repository.
+ public static string? GetRepositoryRoot(string path)
+ {
+ var directory = new DirectoryInfo(path);
+
+ while (directory != null)
+ {
+ var gitDir = Path.Combine(directory.FullName, ".git");
+ if (Directory.Exists(gitDir) || File.Exists(gitDir))
+ {
+ return directory.FullName;
+ }
+ directory = directory.Parent;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Finds the root directory of the Git repository.
+ /// Alias for GetRepositoryRoot for semantic clarity.
+ ///
+ /// A path within the repository.
+ /// The repository root path, or null if not in a Git repository.
+ public static string? FindGitRepositoryRoot(string path) => GetRepositoryRoot(path);
+
+ ///
+ /// Gets the remote origin URL from a Git repository.
+ ///
+ /// The root directory of the Git repository.
+ /// The origin URL, or null if not found.
+ public static string? GetGitRemoteOriginUrl(string repositoryRoot)
+ {
+ var gitDir = Path.Combine(repositoryRoot, ".git");
+ string configPath;
+
+ // Handle both regular .git directory and worktrees (where .git is a file)
+ if (File.Exists(gitDir))
+ {
+ // This is a worktree - .git is a file pointing to the actual git directory
+ var gitDirContent = File.ReadAllText(gitDir).Trim();
+ if (gitDirContent.StartsWith("gitdir:", StringComparison.OrdinalIgnoreCase))
+ {
+ var actualGitDir = gitDirContent.Substring("gitdir:".Length).Trim();
+ // Navigate from worktree git dir to main repo's config
+ // Worktree gitdir points to .git/worktrees/, we need parent .git/config
+ var worktreeDir = new DirectoryInfo(actualGitDir);
+ if (worktreeDir.Parent?.Name == "worktrees" && worktreeDir.Parent?.Parent != null)
+ {
+ configPath = Path.Combine(worktreeDir.Parent.Parent.FullName, "config");
+ }
+ else
+ {
+ configPath = Path.Combine(actualGitDir, "config");
+ }
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else if (Directory.Exists(gitDir))
+ {
+ configPath = Path.Combine(gitDir, "config");
+ }
+ else
+ {
+ return null;
+ }
+
+ if (!File.Exists(configPath))
+ {
+ return null;
+ }
+
+ // Parse the git config file to find the origin URL
+ var lines = File.ReadAllLines(configPath);
+ var inOriginSection = false;
+
+ foreach (var line in lines)
+ {
+ var trimmedLine = line.Trim();
+
+ if (trimmedLine.StartsWith('['))
+ {
+ inOriginSection = trimmedLine.Equals("[remote \"origin\"]", StringComparison.OrdinalIgnoreCase);
+ continue;
+ }
+
+ if (inOriginSection && trimmedLine.StartsWith("url", StringComparison.OrdinalIgnoreCase))
+ {
+ var equalsIndex = trimmedLine.IndexOf('=');
+ if (equalsIndex > 0)
+ {
+ return trimmedLine.Substring(equalsIndex + 1).Trim();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Extracts the repository name from a Git URL.
+ ///
+ /// The Git URL (SSH or HTTPS format).
+ /// The repository name without the .git extension, or null if unable to parse.
+ public static string? ExtractRepositoryNameFromGitUrl(string gitUrl)
+ {
+ if (string.IsNullOrWhiteSpace(gitUrl))
+ {
+ return null;
+ }
+
+ // Handle SSH format: git@github.com:owner/repo.git
+ if (gitUrl.Contains(':') && gitUrl.Contains('@'))
+ {
+ var colonIndex = gitUrl.LastIndexOf(':');
+ var path = gitUrl.Substring(colonIndex + 1);
+ return ExtractRepoNameFromPath(path);
+ }
+
+ // Handle HTTPS format: https://github.com/owner/repo.git
+ if (Uri.TryCreate(gitUrl, UriKind.Absolute, out var uri))
+ {
+ var path = uri.AbsolutePath.TrimStart('/');
+ return ExtractRepoNameFromPath(path);
+ }
+
+ return null;
+ }
+
+ private static string? ExtractRepoNameFromPath(string path)
+ {
+ // Remove .git suffix if present
+ if (path.EndsWith(".git", StringComparison.OrdinalIgnoreCase))
+ {
+ path = path.Substring(0, path.Length - 4);
+ }
+
+ // Get the last segment (repository name)
+ var segments = path.Split('/', '\\');
+ var repoName = segments.LastOrDefault(s => !string.IsNullOrEmpty(s));
+
+ return repoName;
+ }
+}
diff --git a/src/CodeMedic.Abstractions/Plugins/CommandRegistration.cs b/src/CodeMedic.Abstractions/Plugins/CommandRegistration.cs
index e305538..fb50659 100644
--- a/src/CodeMedic.Abstractions/Plugins/CommandRegistration.cs
+++ b/src/CodeMedic.Abstractions/Plugins/CommandRegistration.cs
@@ -20,7 +20,7 @@ public class CommandRegistration
///
/// Gets or sets the command handler that will be executed.
///
- public required Func> Handler { get; init; }
+ public required Func> Handler { get; init; }
///
/// Gets or sets example usage strings for help text.
diff --git a/src/CodeMedic.Abstractions/Plugins/HandlerPayload.cs b/src/CodeMedic.Abstractions/Plugins/HandlerPayload.cs
new file mode 100644
index 0000000..7816cfd
--- /dev/null
+++ b/src/CodeMedic.Abstractions/Plugins/HandlerPayload.cs
@@ -0,0 +1,12 @@
+namespace CodeMedic.Abstractions.Plugins;
+
+///
+/// Payload for command handler execution.
+///
+/// Command-line arguments passed to the handler.
+/// Title of the project being operated on
+/// Renderer instance for output formatting.
+public record struct HandlerPayload(
+ string[] Args,
+ string ProjectTitle,
+ IRenderer Renderer);
diff --git a/src/CodeMedic/Commands/ConfigurationCommandHandler.cs b/src/CodeMedic/Commands/ConfigurationCommandHandler.cs
index 22a3c6e..6fdcf53 100644
--- a/src/CodeMedic/Commands/ConfigurationCommandHandler.cs
+++ b/src/CodeMedic/Commands/ConfigurationCommandHandler.cs
@@ -1,6 +1,7 @@
using CodeMedic.Utilities;
using CodeMedic.Commands;
using CodeMedic.Output;
+using CodeMedic.Abstractions.Plugins;
namespace CodeMedic.Commands;
@@ -98,7 +99,15 @@ private async Task RunCommandsForConfigurationAsync(CodeMedicRunConfiguration co
// Build arguments array to pass the repository path to the command
var commandArgs = new[] { "--path", repoConfig.Path };
- await commandPlugin.Handler(commandArgs, formatter);
+ // setup the handler payload and execute
+ var payload = new HandlerPayload
+ {
+ Args = commandArgs,
+ Renderer = formatter,
+ ProjectTitle = $"CodeMedic Report for {repoConfig.Name} - Command: {commandName}"
+ };
+
+ await commandPlugin.Handler(payload);
}
}
diff --git a/src/CodeMedic/Commands/RootCommandHandler-MCP.cs b/src/CodeMedic/Commands/RootCommandHandler-MCP.cs
index bb22c62..e6b0a6e 100644
--- a/src/CodeMedic/Commands/RootCommandHandler-MCP.cs
+++ b/src/CodeMedic/Commands/RootCommandHandler-MCP.cs
@@ -1,5 +1,6 @@
using System.Text.Json;
using System.Text.Json.Nodes;
+using CodeMedic.Abstractions.Plugins;
using CodeMedic.Output;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
@@ -99,7 +100,14 @@ private static async ValueTask CallTool(RequestContext ProcessArguments(string[] args)
return 0;
}
-
- var (flowControl, value) = await HandleConfigCommand(args, version);
- if (!flowControl)
- {
- return value;
- }
-
// Version requested
if (args.Contains("--version") || args.Contains("-v") || args.Contains("version"))
{
@@ -59,6 +53,13 @@ public static async Task ProcessArguments(string[] args)
return 0;
}
+ // Handle configuration file command
+ var (flowControl, value) = await HandleConfigCommand(args, version);
+ if (!flowControl)
+ {
+ return value;
+ }
+
// Check if a plugin registered this command
var commandName = args[0];
var commandRegistration = _pluginLoader.GetCommand(commandName);
@@ -104,7 +105,12 @@ public static async Task ProcessArguments(string[] args)
"markdown" or "md" => new MarkdownRenderer(),
_ => new ConsoleRenderer()
};
- return await commandRegistration.Handler(commandArgsList.ToArray(), renderer);
+ var handlerPayload = new HandlerPayload(
+ Args: commandArgsList.ToArray(),
+ ProjectTitle: IdentifyProjectTitle(),
+ Renderer: renderer
+ );
+ return await commandRegistration.Handler(handlerPayload);
}
// Unknown command
@@ -113,6 +119,50 @@ public static async Task ProcessArguments(string[] args)
return 1;
}
+ private static string IdentifyProjectTitle()
+ {
+
+ // work through the priorities for identifying project title
+
+ // 1. Solution file in current directory or child directories, if and only if there is exactly one solution file
+ var currentDir = Directory.GetCurrentDirectory();
+ var solutionFiles = Directory.GetFiles(currentDir, "*.sln", SearchOption.AllDirectories);
+ if (solutionFiles.Length == 1)
+ {
+ return Path.GetFileNameWithoutExtension(solutionFiles[0]);
+ }
+
+
+ // 2. Project name from csproj / vbproj / fsproj file in current directory or child directories, if and only if there is exactly one project file
+ var projectFiles = Directory.GetFiles(currentDir, "*.*proj", SearchOption.AllDirectories)
+ .Where(f => f.EndsWith(".csproj") || f.EndsWith(".vbproj") || f.EndsWith(".fsproj"))
+ .ToArray();
+ if (projectFiles.Length == 1)
+ {
+ return Path.GetFileNameWithoutExtension(projectFiles[0]);
+ }
+
+ // 3. Git origin remote repository name, if available
+ var gitDir = GitUtility.FindGitRepositoryRoot(currentDir);
+ if (gitDir != null)
+ {
+ var originUrl = GitUtility.GetGitRemoteOriginUrl(gitDir);
+ if (!string.IsNullOrEmpty(originUrl))
+ {
+ var repoName = GitUtility.ExtractRepositoryNameFromGitUrl(originUrl);
+ if (!string.IsNullOrEmpty(repoName))
+ {
+ return repoName;
+ }
+ }
+ }
+
+ // 4. set project title from current directory name
+ return new DirectoryInfo(currentDir).Name;
+
+
+ }
+
private static async Task<(bool flowControl, int value)> HandleConfigCommand(string[] args, string version)
{
diff --git a/src/CodeMedic/Models/Report/ReportDocument.cs b/src/CodeMedic/Models/Report/ReportDocument.cs
index a17988c..9881160 100644
--- a/src/CodeMedic/Models/Report/ReportDocument.cs
+++ b/src/CodeMedic/Models/Report/ReportDocument.cs
@@ -10,6 +10,11 @@ public class ReportDocument
///
public string Title { get; set; } = string.Empty;
+ ///
+ /// Gets or sets the project name associated with the report.
+ ///
+ public string ProjectName { get; set; } = string.Empty;
+
///
/// Gets or sets the report metadata.
///
diff --git a/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs b/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs
index a1f8c00..b4d562d 100644
--- a/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs
+++ b/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs
@@ -88,8 +88,12 @@ public async Task