using MeetingAssistant.MeetingNotes; using Microsoft.Extensions.Options; namespace MeetingAssistant.Summary; public interface IMeetingSummaryArtifactResolver { Task ResolveBySummaryPathAsync( string summaryPath, CancellationToken cancellationToken); Task ResolveBySummaryPathAsync( string summaryPath, MeetingAssistantOptions options, CancellationToken cancellationToken) { return ResolveBySummaryPathAsync(summaryPath, cancellationToken); } } public sealed class MeetingSummaryArtifactResolver : IMeetingSummaryArtifactResolver { private readonly MeetingAssistantOptions options; private readonly IMeetingNoteStore meetingNoteStore; public MeetingSummaryArtifactResolver( IOptions options, IMeetingNoteStore meetingNoteStore) { this.options = options.Value; this.meetingNoteStore = meetingNoteStore; } public async Task ResolveBySummaryPathAsync( string summaryPath, CancellationToken cancellationToken) { return await ResolveBySummaryPathAsync(summaryPath, options, cancellationToken); } public async Task ResolveBySummaryPathAsync( string summaryPath, MeetingAssistantOptions options, CancellationToken cancellationToken) { var requestedSummaryPath = ResolveRequestedSummaryPath(summaryPath, options); if (requestedSummaryPath is null) { return null; } var meetingNotesFolder = VaultPath.Resolve(options.Vault, options.Vault.MeetingNotesFolder); if (!Directory.Exists(meetingNotesFolder)) { return null; } foreach (var meetingNotePath in Directory.EnumerateFiles(meetingNotesFolder, "*.md")) { var note = await meetingNoteStore.ReadAsync(meetingNotePath, cancellationToken); var noteSummaryPath = ResolveNoteLink(note.Frontmatter.Summary, note.Path); if (!PathsEqual(requestedSummaryPath, noteSummaryPath)) { continue; } return new MeetingSessionArtifacts( note.Path, ResolveNoteLink(note.Frontmatter.Transcript, note.Path), ResolveNoteLink(note.Frontmatter.AssistantContext, note.Path), requestedSummaryPath); } return null; } private static string? ResolveRequestedSummaryPath( string summaryPath, MeetingAssistantOptions options) { if (string.IsNullOrWhiteSpace(summaryPath)) { return null; } var summariesFolder = VaultPath.Resolve(options.Vault, options.Vault.SummariesFolder); var requested = Environment.ExpandEnvironmentVariables(summaryPath); var fullPath = Path.IsPathRooted(requested) ? Path.GetFullPath(requested) : Path.GetFullPath(Path.Combine(summariesFolder, requested)); return IsWithinDirectory(summariesFolder, fullPath) ? fullPath : null; } private static string ResolveNoteLink(string pathOrLink, string sourceNotePath) { if (string.IsNullOrWhiteSpace(pathOrLink)) { return ""; } var sourceFolder = Path.GetDirectoryName(sourceNotePath) ?? ""; var target = ExtractWikiLinkTarget(pathOrLink) ?? pathOrLink; target = target.Replace('/', Path.DirectorySeparatorChar); if (!Path.HasExtension(target)) { target += ".md"; } return Path.GetFullPath(Path.IsPathRooted(target) ? target : Path.Combine(sourceFolder, target)); } private static string? ExtractWikiLinkTarget(string value) { if (!value.StartsWith("[[", StringComparison.Ordinal) || !value.EndsWith("]]", StringComparison.Ordinal)) { return null; } var inner = value[2..^2]; var labelSeparator = inner.IndexOf('|', StringComparison.Ordinal); return labelSeparator < 0 ? inner : inner[..labelSeparator]; } private static bool PathsEqual(string left, string right) { return string.Equals( Path.GetFullPath(left), Path.GetFullPath(right), StringComparison.OrdinalIgnoreCase); } private static bool IsWithinDirectory(string directory, string path) { var normalizedDirectory = Path.GetFullPath(directory) .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; var normalizedPath = Path.GetFullPath(path); return normalizedPath.StartsWith(normalizedDirectory, StringComparison.OrdinalIgnoreCase); } }