Public Access
Generalize settings and logs assistant
PR and Push Build/Test / build-and-test (push) Successful in 16m43s
PR and Push Build/Test / build-and-test (push) Successful in 16m43s
This commit is contained in:
@@ -35,22 +35,22 @@ public sealed class MeetingSummaryTools
|
||||
projectResolver = new BoundMeetingProjectResolver(options);
|
||||
}
|
||||
|
||||
public Task<string> ReadTranscript(int? @from = null, int? to = null)
|
||||
public Task<string> ReadTranscript(int? @from = null, int? to = null, int? tail = null)
|
||||
{
|
||||
return ReadIfExistsAsync(artifacts.TranscriptPath, @from, to);
|
||||
return ReadIfExistsAsync(artifacts.TranscriptPath, @from, to, tail);
|
||||
}
|
||||
|
||||
public Task<string> ReadMeetingNote(int? @from = null, int? to = null)
|
||||
public Task<string> ReadMeetingNote(int? @from = null, int? to = null, int? tail = null)
|
||||
{
|
||||
return ReadIfExistsAsync(artifacts.MeetingNotePath, @from, to);
|
||||
return ReadIfExistsAsync(artifacts.MeetingNotePath, @from, to, tail);
|
||||
}
|
||||
|
||||
public Task<string> ReadContext(int? @from = null, int? to = null)
|
||||
public Task<string> ReadContext(int? @from = null, int? to = null, int? tail = null)
|
||||
{
|
||||
return ReadIfExistsAsync(artifacts.AssistantContextPath, @from, to);
|
||||
return ReadIfExistsAsync(artifacts.AssistantContextPath, @from, to, tail);
|
||||
}
|
||||
|
||||
public async Task<string> ReadUserNotes(int? @from = null, int? to = null)
|
||||
public async Task<string> ReadUserNotes(int? @from = null, int? to = null, int? tail = null)
|
||||
{
|
||||
if (!File.Exists(artifacts.MeetingNotePath))
|
||||
{
|
||||
@@ -60,12 +60,12 @@ public sealed class MeetingSummaryTools
|
||||
var content = await File.ReadAllTextAsync(artifacts.MeetingNotePath);
|
||||
var document = MarkdownDocumentParser.SplitOptional(content);
|
||||
var body = document.HasFrontmatter ? document.Body.Trim() : content;
|
||||
return ReadLines(body, @from, to);
|
||||
return AgentFileToolContent.ReadLines(body, @from, to, tail);
|
||||
}
|
||||
|
||||
public Task<string> ReadGlossary(int? @from = null, int? to = null)
|
||||
public Task<string> ReadGlossary(int? @from = null, int? to = null, int? tail = null)
|
||||
{
|
||||
return ReadIfExistsAsync(VaultPath.Resolve(options.Vault, options.Vault.DictationWordsPath), @from, to);
|
||||
return ReadIfExistsAsync(VaultPath.Resolve(options.Vault, options.Vault.DictationWordsPath), @from, to, tail);
|
||||
}
|
||||
|
||||
public async Task<string> AddDictationWord(string word)
|
||||
@@ -232,7 +232,7 @@ public sealed class MeetingSummaryTools
|
||||
artifacts.SummaryPath,
|
||||
MeetingArtifactFrontmatterRenderer.Render(frontmatter, markdown));
|
||||
SummaryWasWritten = true;
|
||||
return artifacts.SummaryPath;
|
||||
return Path.GetFileName(artifacts.SummaryPath);
|
||||
}
|
||||
|
||||
private static bool ContainsLineBreak(string value)
|
||||
@@ -250,7 +250,7 @@ public sealed class MeetingSummaryTools
|
||||
int? insert = null,
|
||||
bool replace_file = false)
|
||||
{
|
||||
var editMode = FileLineEditMode.Create(@from, to, insert, replace_file);
|
||||
var editMode = AgentFileEditMode.Create(@from, to, insert, replace_file);
|
||||
if (editMode is null)
|
||||
{
|
||||
return "Refused: supply either both from and to for replacement, insert for insertion, or no line arguments for append; set replace_file=true only for whole-file replacement.";
|
||||
@@ -263,7 +263,7 @@ public sealed class MeetingSummaryTools
|
||||
|
||||
var existing = await File.ReadAllTextAsync(artifacts.AssistantContextPath);
|
||||
var (frontmatter, body) = MeetingArtifactFrontmatterRenderer.Split(existing);
|
||||
var updatedBody = ApplyLineEdit(body, StripAccidentalFrontmatter(content), editMode);
|
||||
var updatedBody = AgentFileToolContent.ApplyLineEdit(body, StripAccidentalFrontmatter(content), editMode);
|
||||
var updated = string.IsNullOrWhiteSpace(frontmatter)
|
||||
? updatedBody
|
||||
: "---" + Environment.NewLine + frontmatter + Environment.NewLine + "---" + Environment.NewLine + Environment.NewLine + updatedBody;
|
||||
@@ -293,12 +293,12 @@ public sealed class MeetingSummaryTools
|
||||
}
|
||||
|
||||
var files = Directory.EnumerateFiles(projectRoot, "*", SearchOption.AllDirectories)
|
||||
.Select(path => ToToolPath(Path.GetRelativePath(projectRoot, path)))
|
||||
.Select(path => AgentFileToolContent.ToToolPath(Path.GetRelativePath(projectRoot, path)))
|
||||
.Order(StringComparer.Ordinal);
|
||||
return string.Join('\n', files);
|
||||
}
|
||||
|
||||
public async Task<string> ReadProjectFile(string project, string path, int? @from = null, int? to = null)
|
||||
public async Task<string> ReadProjectFile(string project, string path, int? @from = null, int? to = null, int? tail = null)
|
||||
{
|
||||
var filePath = await ResolveBoundProjectFilePathAsync(project, path);
|
||||
if (filePath is null || !File.Exists(filePath))
|
||||
@@ -306,7 +306,7 @@ public sealed class MeetingSummaryTools
|
||||
return "";
|
||||
}
|
||||
|
||||
return ReadLines(await File.ReadAllTextAsync(filePath), @from, to);
|
||||
return AgentFileToolContent.ReadLines(await File.ReadAllTextAsync(filePath), @from, to, tail);
|
||||
}
|
||||
|
||||
public async Task<string> ListPastProjectMeetings(
|
||||
@@ -338,7 +338,7 @@ public sealed class MeetingSummaryTools
|
||||
return string.Join('\n', lines);
|
||||
}
|
||||
|
||||
public async Task<string> ReadPastProjectMeetingSummary(string path, int? @from = null, int? to = null)
|
||||
public async Task<string> ReadPastProjectMeetingSummary(string path, int? @from = null, int? to = null, int? tail = null)
|
||||
{
|
||||
var summary = await ResolvePastSummaryAsync(path);
|
||||
if (summary.Refusal is not null)
|
||||
@@ -346,7 +346,7 @@ public sealed class MeetingSummaryTools
|
||||
return summary.Refusal;
|
||||
}
|
||||
|
||||
return ReadLines(await File.ReadAllTextAsync(summary.Summary!.Path), @from, to);
|
||||
return AgentFileToolContent.ReadLines(await File.ReadAllTextAsync(summary.Summary!.Path), @from, to, tail);
|
||||
}
|
||||
|
||||
public async Task<string> WriteProjectFile(
|
||||
@@ -358,7 +358,7 @@ public sealed class MeetingSummaryTools
|
||||
int? insert = null,
|
||||
bool replace_file = false)
|
||||
{
|
||||
var editMode = FileLineEditMode.Create(@from, to, insert, replace_file);
|
||||
var editMode = AgentFileEditMode.Create(@from, to, insert, replace_file);
|
||||
if (editMode is null)
|
||||
{
|
||||
return "Refused: supply either both from and to for replacement, insert for insertion, or no line arguments for append; set replace_file=true only for whole-file replacement.";
|
||||
@@ -375,17 +375,17 @@ public sealed class MeetingSummaryTools
|
||||
{
|
||||
await writeAudit.CaptureFileWriteAsync(
|
||||
target.Path,
|
||||
() => WriteProjectFileContentAsync(target.Path, content, editMode));
|
||||
() => AgentFileToolContent.WriteFileContentAsync(target.Path, content, editMode));
|
||||
}
|
||||
else
|
||||
{
|
||||
await WriteProjectFileContentAsync(target.Path, content, editMode);
|
||||
await AgentFileToolContent.WriteFileContentAsync(target.Path, content, editMode);
|
||||
}
|
||||
|
||||
return $"{target.Project.Name}/{ToToolPath(path)}";
|
||||
return $"{target.Project.Name}/{AgentFileToolContent.ToToolPath(path)}";
|
||||
}
|
||||
|
||||
public async Task<string> Search(string keywords, string[]? projects = null)
|
||||
public async Task<string> Search(string keywords, string[]? projects = null, string? file_pattern = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(keywords))
|
||||
{
|
||||
@@ -400,10 +400,10 @@ public sealed class MeetingSummaryTools
|
||||
|
||||
var selectedProjects = await GetExistingProjectsInScopeAsync(scope.ProjectNames);
|
||||
var summaries = await GetScopedPastSummariesAsync(scope.ProjectNames);
|
||||
return SearchSources(selectedProjects, summaries, keywords, selectedProjects.Count == 1);
|
||||
return SearchSources(selectedProjects, summaries, keywords, selectedProjects.Count == 1, file_pattern);
|
||||
}
|
||||
|
||||
private static async Task<string> ReadIfExistsAsync(string path, int? from = null, int? to = null)
|
||||
private static async Task<string> ReadIfExistsAsync(string path, int? from = null, int? to = null, int? tail = null)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
@@ -411,7 +411,7 @@ public sealed class MeetingSummaryTools
|
||||
}
|
||||
|
||||
var content = await File.ReadAllTextAsync(path);
|
||||
return ReadLines(content, from, to);
|
||||
return AgentFileToolContent.ReadLines(content, from, to, tail);
|
||||
}
|
||||
|
||||
private async Task<MeetingNote> ReadMeetingNoteAsync()
|
||||
@@ -575,7 +575,7 @@ public sealed class MeetingSummaryTools
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(Path.Combine(projectRoot, path));
|
||||
return IsWithinDirectory(projectRoot, fullPath) ? fullPath : null;
|
||||
return AgentFileToolContent.IsWithinDirectory(projectRoot, fullPath) ? fullPath : null;
|
||||
}
|
||||
|
||||
private ProjectFileTarget? ResolveExistingProjectFilePath(string project, string path)
|
||||
@@ -602,147 +602,11 @@ public sealed class MeetingSummaryTools
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(Path.Combine(projectFolder.Path, path));
|
||||
return IsWithinDirectory(projectFolder.Path, fullPath)
|
||||
return AgentFileToolContent.IsWithinDirectory(projectFolder.Path, fullPath)
|
||||
? new ProjectFileTarget(projectFolder, fullPath)
|
||||
: null;
|
||||
}
|
||||
|
||||
private static async Task WriteProjectFileContentAsync(
|
||||
string filePath,
|
||||
string content,
|
||||
FileLineEditMode editMode)
|
||||
{
|
||||
if (editMode.Kind == FileLineEditKind.Overwrite)
|
||||
{
|
||||
await File.WriteAllTextAsync(filePath, content);
|
||||
return;
|
||||
}
|
||||
|
||||
if (editMode.Kind == FileLineEditKind.Append)
|
||||
{
|
||||
var existing = File.Exists(filePath)
|
||||
? await File.ReadAllTextAsync(filePath)
|
||||
: "";
|
||||
await File.WriteAllTextAsync(filePath, AppendContent(existing, content));
|
||||
return;
|
||||
}
|
||||
|
||||
var lines = File.Exists(filePath)
|
||||
? (await File.ReadAllLinesAsync(filePath)).ToList()
|
||||
: [];
|
||||
var replacementLines = SplitLines(content);
|
||||
|
||||
if (editMode.Kind == FileLineEditKind.Insert)
|
||||
{
|
||||
var insertIndex = Math.Clamp(editMode.Insert!.Value - 1, 0, lines.Count);
|
||||
lines.InsertRange(insertIndex, replacementLines);
|
||||
await File.WriteAllTextAsync(filePath, string.Join('\n', lines));
|
||||
return;
|
||||
}
|
||||
|
||||
var startIndex = Math.Clamp(editMode.From!.Value - 1, 0, lines.Count);
|
||||
var endIndexExclusive = Math.Clamp(editMode.To!.Value, 0, lines.Count);
|
||||
if (endIndexExclusive < startIndex)
|
||||
{
|
||||
endIndexExclusive = startIndex;
|
||||
}
|
||||
|
||||
lines.RemoveRange(startIndex, endIndexExclusive - startIndex);
|
||||
lines.InsertRange(startIndex, replacementLines);
|
||||
await File.WriteAllTextAsync(filePath, string.Join('\n', lines));
|
||||
}
|
||||
|
||||
private static List<string> SplitLines(string content)
|
||||
{
|
||||
if (content.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return content
|
||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Replace('\r', '\n')
|
||||
.Split('\n')
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string ReadLines(string content, int? from = null, int? to = null)
|
||||
{
|
||||
if (!from.HasValue && !to.HasValue)
|
||||
{
|
||||
return content;
|
||||
}
|
||||
|
||||
var lines = SplitLines(content);
|
||||
if (lines.Count == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
var start = Math.Clamp((from ?? 1) - 1, 0, lines.Count - 1);
|
||||
var end = Math.Clamp((to ?? lines.Count) - 1, 0, lines.Count - 1);
|
||||
if (end < start)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return string.Join('\n', lines.GetRange(start, end - start + 1));
|
||||
}
|
||||
|
||||
private static string ApplyLineEdit(
|
||||
string existingContent,
|
||||
string replacementContent,
|
||||
FileLineEditMode editMode)
|
||||
{
|
||||
if (editMode.Kind == FileLineEditKind.Overwrite)
|
||||
{
|
||||
return replacementContent;
|
||||
}
|
||||
|
||||
if (editMode.Kind == FileLineEditKind.Append)
|
||||
{
|
||||
return AppendContent(existingContent, replacementContent);
|
||||
}
|
||||
|
||||
var lines = SplitLines(existingContent);
|
||||
var replacementLines = SplitLines(replacementContent);
|
||||
if (editMode.Kind == FileLineEditKind.Insert)
|
||||
{
|
||||
var insertIndex = Math.Clamp(editMode.Insert!.Value - 1, 0, lines.Count);
|
||||
lines.InsertRange(insertIndex, replacementLines);
|
||||
return string.Join('\n', lines);
|
||||
}
|
||||
|
||||
var startIndex = Math.Clamp(editMode.From!.Value - 1, 0, lines.Count);
|
||||
var endIndexExclusive = Math.Clamp(editMode.To!.Value, 0, lines.Count);
|
||||
if (endIndexExclusive < startIndex)
|
||||
{
|
||||
endIndexExclusive = startIndex;
|
||||
}
|
||||
|
||||
lines.RemoveRange(startIndex, endIndexExclusive - startIndex);
|
||||
lines.InsertRange(startIndex, replacementLines);
|
||||
return string.Join('\n', lines);
|
||||
}
|
||||
|
||||
private static string AppendContent(string existingContent, string content)
|
||||
{
|
||||
if (string.IsNullOrEmpty(existingContent))
|
||||
{
|
||||
return content;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
return existingContent;
|
||||
}
|
||||
|
||||
var separator = existingContent.EndsWith('\n') || existingContent.EndsWith('\r')
|
||||
? ""
|
||||
: "\n";
|
||||
return existingContent + separator + content;
|
||||
}
|
||||
|
||||
private Task<List<BoundMeetingProject>> GetBoundProjectsAsync()
|
||||
{
|
||||
return projectResolver.GetBoundProjectsAsync(artifacts);
|
||||
@@ -757,7 +621,8 @@ public sealed class MeetingSummaryTools
|
||||
IReadOnlyList<BoundMeetingProject> projects,
|
||||
IReadOnlyList<PastMeetingSummary> summaries,
|
||||
string keywords,
|
||||
bool singleProject)
|
||||
bool singleProject,
|
||||
string? filePattern)
|
||||
{
|
||||
System.Text.RegularExpressions.Regex regex;
|
||||
try
|
||||
@@ -776,7 +641,13 @@ public sealed class MeetingSummaryTools
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(project.Path, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relative = Path.Combine(project.Name, Path.GetRelativePath(project.Path, file));
|
||||
var projectRelative = AgentFileToolContent.ToToolPath(Path.GetRelativePath(project.Path, file));
|
||||
if (!AgentFileToolContent.MatchesGlob(projectRelative, filePattern))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var relative = Path.Combine(project.Name, projectRelative);
|
||||
AddFileMatches(matches, regex, file, FormatSearchPath(relative, singleProject));
|
||||
}
|
||||
}
|
||||
@@ -805,14 +676,14 @@ public sealed class MeetingSummaryTools
|
||||
{
|
||||
if (regex.IsMatch(lines[index]))
|
||||
{
|
||||
matches.Add($"{ToToolPath(formattedPath)}:{index + 1} {lines[index]}");
|
||||
matches.Add($"{AgentFileToolContent.ToToolPath(formattedPath)}:{index + 1} {lines[index]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatSearchPath(string path, bool singleProject)
|
||||
{
|
||||
var formatted = ToToolPath(path);
|
||||
var formatted = AgentFileToolContent.ToToolPath(path);
|
||||
if (!singleProject)
|
||||
{
|
||||
return formatted;
|
||||
@@ -822,20 +693,6 @@ public sealed class MeetingSummaryTools
|
||||
return slashIndex < 0 ? formatted : formatted[(slashIndex + 1)..];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static string ToToolPath(string path)
|
||||
{
|
||||
return path.Replace(Path.DirectorySeparatorChar.ToString(), "/", StringComparison.Ordinal)
|
||||
.Replace(Path.AltDirectorySeparatorChar.ToString(), "/", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private async Task<List<PastMeetingSummary>> GetScopedPastSummariesAsync(IReadOnlySet<string> projectNames)
|
||||
{
|
||||
if (projectNames.Count == 0)
|
||||
@@ -885,7 +742,7 @@ public sealed class MeetingSummaryTools
|
||||
|
||||
var summariesRoot = GetSummariesRoot();
|
||||
var fullPath = Path.GetFullPath(Path.Combine(summariesRoot, path));
|
||||
if (!IsWithinDirectory(summariesRoot, fullPath) || !File.Exists(fullPath))
|
||||
if (!AgentFileToolContent.IsWithinDirectory(summariesRoot, fullPath) || !File.Exists(fullPath))
|
||||
{
|
||||
return PastMeetingSummaryResolution.Refused("Refused: summary file does not exist under the configured summaries folder.");
|
||||
}
|
||||
@@ -911,7 +768,7 @@ public sealed class MeetingSummaryTools
|
||||
|
||||
private static string NormalizePastMeetingSummaryToolPath(string path)
|
||||
{
|
||||
var toolPath = ToToolPath(path.Trim());
|
||||
var toolPath = AgentFileToolContent.ToToolPath(path.Trim());
|
||||
const string pastMeetingPrefix = "past-meetings/";
|
||||
return toolPath.StartsWith(pastMeetingPrefix, StringComparison.OrdinalIgnoreCase)
|
||||
? toolPath[pastMeetingPrefix.Length..]
|
||||
@@ -950,7 +807,7 @@ public sealed class MeetingSummaryTools
|
||||
|
||||
return new PastMeetingSummary(
|
||||
path,
|
||||
ToToolPath(Path.GetRelativePath(summariesRoot, path)),
|
||||
AgentFileToolContent.ToToolPath(Path.GetRelativePath(summariesRoot, path)),
|
||||
string.IsNullOrWhiteSpace(frontmatter.Title) ? Path.GetFileNameWithoutExtension(path) : frontmatter.Title.Trim(),
|
||||
ParseDateTime(frontmatter.StartTime),
|
||||
projects);
|
||||
@@ -985,45 +842,6 @@ public sealed class MeetingSummaryTools
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record FileLineEditMode(
|
||||
FileLineEditKind Kind,
|
||||
int? From = null,
|
||||
int? To = null,
|
||||
int? Insert = null)
|
||||
{
|
||||
public static FileLineEditMode? Create(int? from, int? to, int? insert, bool replaceFile)
|
||||
{
|
||||
if (replaceFile && (from.HasValue || to.HasValue || insert.HasValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (insert.HasValue)
|
||||
{
|
||||
return from.HasValue || to.HasValue
|
||||
? null
|
||||
: new FileLineEditMode(FileLineEditKind.Insert, Insert: insert);
|
||||
}
|
||||
|
||||
if (from.HasValue || to.HasValue)
|
||||
{
|
||||
return from.HasValue && to.HasValue && from.Value <= to.Value
|
||||
? new FileLineEditMode(FileLineEditKind.Replace, From: from, To: to)
|
||||
: null;
|
||||
}
|
||||
|
||||
return new FileLineEditMode(replaceFile ? FileLineEditKind.Overwrite : FileLineEditKind.Append);
|
||||
}
|
||||
}
|
||||
|
||||
private enum FileLineEditKind
|
||||
{
|
||||
Append,
|
||||
Overwrite,
|
||||
Replace,
|
||||
Insert
|
||||
}
|
||||
|
||||
private sealed class MeetingNoteFrontmatterYaml
|
||||
{
|
||||
[YamlDotNet.Serialization.YamlMember(Alias = "title")]
|
||||
|
||||
Reference in New Issue
Block a user