Skip to content

Commit 0de8bc9

Browse files
committed
Add demo gif, modify validator flags, add strict mode, track token usage, improve agent searching and more!
1 parent 1711c9c commit 0de8bc9

7 files changed

Lines changed: 921 additions & 131 deletions

File tree

GitRekt/AiValidation.cs

Lines changed: 722 additions & 29 deletions
Large diffs are not rendered by default.

GitRekt/GitRektCli.cs

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ internal static class GitRektCli
4040

4141
var validateAiOption = new Option<bool>("--ai", "Validate each displayed result with AI.");
4242

43-
var aiAgentOption = new Option<bool>("--ai-agent", "Use Microsoft Agent Framework to gather same-repository evidence before AI validation.");
43+
var aiAgentOption = new Option<bool>("--ai-agent", "Gather same-repository evidence before AI validation.");
4444

4545
var aiProviderOption = new Option<string?>("--ai-provider", "AI provider for validation. Defaults to ollama. Supported: ollama, gemini, openai.");
4646

@@ -50,13 +50,15 @@ internal static class GitRektCli
5050
aiApiKeyOption.AddAlias("--gemini-api-key");
5151
aiApiKeyOption.AddAlias("--openai-api-key");
5252

53-
var aiVerdictOption = new Option<string?>("--ai-verdict", "Only show AI validation verdicts at or above this category. Values: likely/red, possible/yellow, none/green.")
53+
var aiVerdictOption = new Option<string?>("--ai-verdict", "Only show AI validation verdicts at or above this category. Values: likely, possible, none.")
5454
{
5555
Arity = ArgumentArity.ZeroOrOne
5656
};
5757
aiVerdictOption.AddAlias("--ai-category");
5858
aiVerdictOption.AddAlias("--ai-show");
5959

60+
var strictOption = new Option<bool>("--strict", "Use stricter AI validation. Ordinary business contact lists are treated as low signal unless stronger sensitive data is present.");
61+
6062
var rootCommand = new RootCommand("""
6163
Search GitHub code.
6264
@@ -67,8 +69,9 @@ Search GitHub code.
6769
GitRekt --query "Password1" --query "Password2"
6870
GitRekt --advanced --query "\"Password1\" language:C# path:/src/" --query "\"Password2\" language:C#"
6971
GitRekt --query "Password1" --ai --ai-model llama3.2
70-
GitRekt --query "Password1" --ai --ai-model llama3.2 --ai-verdict yellow
71-
GitRekt --query "Password1" --ai --ai-agent --ai-model llama3.2 --ai-verdict yellow
72+
GitRekt --query "Password1" --ai --ai-model llama3.2 --ai-verdict possible
73+
GitRekt --query "@example.com" --ai --strict --ai-model llama3.2 --ai-verdict possible
74+
GitRekt --query "Password1" --ai --ai-agent --ai-model llama3.2 --ai-verdict possible
7275
GitRekt --query "Password1" --ai-provider gemini --ai-model gemini-2.5-flash
7376
GitRekt --query "Password1" --ai-provider openai --ai-model gpt-5-mini
7477
GitRekt --github-app-id 12345 --github-app-private-key-path app.pem --query "Password1"
@@ -88,6 +91,7 @@ Search GitHub code.
8891
rootCommand.AddOption(aiModelOption);
8992
rootCommand.AddOption(aiApiKeyOption);
9093
rootCommand.AddOption(aiVerdictOption);
94+
rootCommand.AddOption(strictOption);
9195
rootCommand.AddValidator(parseResult =>
9296
{
9397
if (IsHelpRequested(args))
@@ -109,8 +113,9 @@ Search GitHub code.
109113
var hasAiModel = parseResult.FindResultFor(aiModelOption) is not null;
110114
var hasAiApiKey = parseResult.FindResultFor(aiApiKeyOption) is not null;
111115
var hasAiVerdictFilter = !string.IsNullOrWhiteSpace(parseResult.GetValueForOption(aiVerdictOption));
116+
var hasStrict = parseResult.GetValueForOption(strictOption);
112117

113-
if (!hasAiFlag && !hasAiAgentFlag && !hasAiProvider && !hasAiModel && !hasAiApiKey && !hasAiVerdictFilter)
118+
if (!hasAiFlag && !hasAiAgentFlag && !hasAiProvider && !hasAiModel && !hasAiApiKey && !hasAiVerdictFilter && !hasStrict)
114119
{
115120
return;
116121
}
@@ -131,14 +136,6 @@ Search GitHub code.
131136
return;
132137
}
133138

134-
if (parseResult.GetValueForOption(aiAgentOption)
135-
&& !string.IsNullOrWhiteSpace(aiProvider)
136-
&& !string.Equals(aiProvider, "ollama", StringComparison.OrdinalIgnoreCase))
137-
{
138-
parseResult.ErrorMessage = "--ai-agent is currently only supported with --ai-provider ollama.";
139-
return;
140-
}
141-
142139
if (string.Equals(aiProvider, "gemini", StringComparison.OrdinalIgnoreCase))
143140
{
144141
var aiApiKey = parseResult.GetValueForOption(aiApiKeyOption)
@@ -169,7 +166,7 @@ Search GitHub code.
169166
if (!string.IsNullOrWhiteSpace(aiVerdict)
170167
&& !TryParseAiValidationVerdict(aiVerdict, out _))
171168
{
172-
parseResult.ErrorMessage = $"Unsupported --ai-verdict value '{aiVerdict}'. Supported values: likely/red, possible/yellow, none/green.";
169+
parseResult.ErrorMessage = $"Unsupported --ai-verdict value '{aiVerdict}'. Supported values: likely, possible, none.";
173170
return;
174171
}
175172
});
@@ -191,13 +188,15 @@ Search GitHub code.
191188
var aiModel = parseResult.GetValueForOption(aiModelOption);
192189
var aiApiKey = parseResult.GetValueForOption(aiApiKeyOption);
193190
var aiVerdicts = parseResult.GetValueForOption(aiVerdictOption);
191+
var strictMode = parseResult.GetValueForOption(strictOption);
194192
var aiVerdictFilter = ParseAiValidationVerdict(aiVerdicts);
195193
var shouldValidateAi = validateAi
196194
|| useAiAgent
197195
|| !string.IsNullOrWhiteSpace(aiProvider)
198196
|| !string.IsNullOrWhiteSpace(aiModel)
199197
|| !string.IsNullOrWhiteSpace(aiApiKey)
200-
|| aiVerdictFilter is not null;
198+
|| aiVerdictFilter is not null
199+
|| strictMode;
201200

202201
if (queries.Length == 0)
203202
{
@@ -216,11 +215,6 @@ Search GitHub code.
216215

217216
aiProvider = string.IsNullOrWhiteSpace(aiProvider) ? "ollama" : aiProvider;
218217

219-
if (useAiAgent && !string.Equals(aiProvider, "ollama", StringComparison.OrdinalIgnoreCase))
220-
{
221-
throw new InvalidOperationException("--ai-agent is currently only supported with --ai-provider ollama.");
222-
}
223-
224218
aiApiKey ??= string.Equals(aiProvider, "gemini", StringComparison.OrdinalIgnoreCase)
225219
? Environment.GetEnvironmentVariable("GEMINI_API_KEY") ?? Environment.GetEnvironmentVariable("GOOGLE_API_KEY")
226220
: string.Equals(aiProvider, "openai", StringComparison.OrdinalIgnoreCase)
@@ -269,6 +263,7 @@ Search GitHub code.
269263
aiProvider,
270264
aiModel!,
271265
useAiAgent,
266+
strictMode,
272267
aiApiKey)
273268
: null,
274269
aiVerdictFilter);
@@ -312,7 +307,7 @@ Search GitHub code.
312307

313308
return TryParseAiValidationVerdict(value, out var verdict)
314309
? verdict
315-
: throw new InvalidOperationException($"Unsupported --ai-verdict value '{value}'. Supported values: likely/red, possible/yellow, none/green.");
310+
: throw new InvalidOperationException($"Unsupported --ai-verdict value '{value}'. Supported values: likely, possible, none.");
316311
}
317312

318313
private static bool TryParseAiValidationVerdict(string? value, out AiValidationVerdict verdict)
@@ -449,12 +444,13 @@ Search GitHub code.
449444
-o, --output <path> Write output to a file.
450445
--advanced, --raw-query Pass each query to GitHub code search unchanged.
451446
--ai Validate each displayed result with AI.
452-
--ai-agent Use Microsoft Agent Framework to gather same-repository evidence before AI validation.
447+
--ai-agent Gather same-repository evidence before AI validation.
453448
--ai-provider <provider> AI provider for validation. Supported: ollama, gemini, openai.
454449
--ai-model <model> Model name used for AI validation.
455450
--ai-api-key, --gemini-api-key, --openai-api-key <key>
456451
AI provider API key. For Gemini, defaults to GEMINI_API_KEY or GOOGLE_API_KEY. For OpenAI, defaults to OPENAI_API_KEY.
457-
--ai-verdict <verdict> Only show AI verdicts at or above likely/red, possible/yellow, or none/green.
452+
--ai-verdict <verdict> Only show AI verdicts at or above likely, possible, or none.
453+
--strict Use stricter AI validation. Ordinary business contact lists and low value PII are ignored.
458454
-h, --help Show help.
459455
460456
Examples:

GitRekt/GithubClient.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,10 @@ internal sealed record GithubCodeSearchRepository(
727727
string FullName,
728728

729729
[property: JsonPropertyName("html_url")]
730-
string? HtmlUrl);
730+
string? HtmlUrl,
731+
732+
[property: JsonPropertyName("default_branch")]
733+
string? DefaultBranch = null);
731734

732735
internal sealed record GithubTextMatch(
733736
[property: JsonPropertyName("object_type")]

GitRekt/Program.cs

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using GitRekt;
2+
using System.Security.Cryptography;
23
using System.Text;
34

45
Console.OutputEncoding = Encoding.UTF8;
@@ -37,6 +38,7 @@
3738
var hasAiVerdictFilter = cliArguments.AiValidationVerdictFilter is not null;
3839
var useAiAgent = cliArguments.AiValidation?.UseAgent == true;
3940
var aiValidationCache = new Dictionary<string, AiValidationCacheValue>(StringComparer.OrdinalIgnoreCase);
41+
var totalDisplayedResults = 0;
4042

4143
foreach (var (query, queryIndex) in cliArguments.Queries.Select((query, index) => (query, index)))
4244
{
@@ -45,6 +47,7 @@
4547
var validatedResultIndex = 0;
4648
var hasSearchResults = false;
4749
var hasDisplayedResults = false;
50+
var duplicateSignatures = new HashSet<string>(StringComparer.Ordinal);
4851

4952
if (isMultiQuery)
5053
{
@@ -64,13 +67,20 @@
6467

6568
while (resultIndex < searchPage.Items.Count)
6669
{
70+
if (IsDuplicateResult(searchPage.Items[resultIndex], duplicateSignatures))
71+
{
72+
resultIndex++;
73+
continue;
74+
}
75+
6776
var chunkValidationOutcomes = await ValidateNextSearchResultChunkAsync(
6877
aiValidationClient,
6978
query,
7079
cliArguments.UseAdvancedQuery,
7180
searchPage,
7281
resultIndex,
7382
validatedResultIndex,
83+
duplicateSignatures,
7484
aiValidationCache);
7585
validatedResultIndex = chunkValidationOutcomes.ValidatedResultIndex;
7686

@@ -88,6 +98,7 @@
8898
}
8999

90100
displayedResultIndex++;
101+
totalDisplayedResults++;
91102
hasDisplayedResults = true;
92103
var header = hasAiVerdictFilter
93104
? $"Result {validationOutcome?.ValidatedIndex ?? displayedResultIndex}/{searchPage.AvailableCount}"
@@ -123,6 +134,8 @@ await WriteSearchResultAsync(
123134
}
124135
}
125136

137+
WriteCompletionSummary(totalDisplayedResults, aiValidationClient?.TokenUsage, outputWriter, statusLine);
138+
126139
return 0;
127140
}
128141
catch (Exception ex)
@@ -150,13 +163,44 @@ await WriteSearchResultAsync(
150163
return new StreamWriter(fullPath, append: false);
151164
}
152165

166+
static void WriteCompletionSummary(int displayedResultCount, AiTokenUsage? tokenUsage, TextWriter? outputWriter, ConsoleStatusLine statusLine)
167+
{
168+
var resultText = displayedResultCount == 1
169+
? "1 result"
170+
: $"{displayedResultCount.ToString(System.Globalization.CultureInfo.InvariantCulture)} results";
171+
172+
if (tokenUsage is null)
173+
{
174+
WriteColoredLine($"Done. Displayed {resultText}.", ConsoleColor.Cyan, outputWriter, statusLine);
175+
return;
176+
}
177+
178+
if (!tokenUsage.HasUsage)
179+
{
180+
WriteColoredLine($"Done. Displayed {resultText}. AI token usage was not reported by the provider.", ConsoleColor.Cyan, outputWriter, statusLine);
181+
return;
182+
}
183+
184+
WriteColoredLine(
185+
$"Done. Displayed {resultText}. AI tokens: {FormatTokenCount(tokenUsage.TotalTokens)} total ({FormatTokenCount(tokenUsage.InputTokens)} input, {FormatTokenCount(tokenUsage.OutputTokens)} output).",
186+
ConsoleColor.Cyan,
187+
outputWriter,
188+
statusLine);
189+
}
190+
191+
static string FormatTokenCount(long tokenCount)
192+
{
193+
return tokenCount.ToString("N0", System.Globalization.CultureInfo.InvariantCulture);
194+
}
195+
153196
static async Task<AiValidationPageOutcomes> ValidateNextSearchResultChunkAsync(
154197
IAiValidationClient? aiValidationClient,
155198
string query,
156199
bool useAdvancedQuery,
157200
GithubCodeSearchPage searchPage,
158201
int startIndex,
159202
int validatedResultIndex,
203+
HashSet<string> duplicateSignatures,
160204
Dictionary<string, AiValidationCacheValue> aiValidationCache)
161205
{
162206
const int MaxStreamingRepositoryBatchSize = 8;
@@ -199,6 +243,7 @@ [new AiValidationOutcome(cacheValue.Validation, cacheValue.Error, validatedResul
199243
searchPage,
200244
startIndex,
201245
MaxStreamingRepositoryBatchSize,
246+
duplicateSignatures,
202247
aiValidationCache);
203248

204249
if (pendingResults.Count > 1)
@@ -260,6 +305,7 @@ static List<PendingAiValidationResult> CollectStreamingRepositoryBatch(
260305
GithubCodeSearchPage searchPage,
261306
int startIndex,
262307
int maxBatchSize,
308+
HashSet<string> duplicateSignatures,
263309
Dictionary<string, AiValidationCacheValue> aiValidationCache)
264310
{
265311
var firstResult = searchPage.Items[startIndex];
@@ -281,6 +327,11 @@ static List<PendingAiValidationResult> CollectStreamingRepositoryBatch(
281327
break;
282328
}
283329

330+
if (index != startIndex && IsDuplicateResult(result, duplicateSignatures))
331+
{
332+
break;
333+
}
334+
284335
var cacheKey = CreateAiValidationCacheKey(query, useAdvancedQuery, result);
285336

286337
if (aiValidationCache.ContainsKey(cacheKey))
@@ -412,6 +463,34 @@ static string NormalizeSnippet(string snippet)
412463
return string.Join(' ', snippet.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
413464
}
414465

466+
static string CreateResultDuplicateSignature(GithubCodeSearchResult result)
467+
{
468+
var snippetSignature = CreateSnippetSignature(result);
469+
470+
if (string.IsNullOrWhiteSpace(snippetSignature))
471+
{
472+
return string.Empty;
473+
}
474+
475+
var signatureBytes = Encoding.UTF8.GetBytes(string.Join('\n', result.Repository.FullName, snippetSignature));
476+
var signatureHash = SHA256.HashData(signatureBytes);
477+
return Convert.ToHexString(signatureHash.AsSpan(0, 16));
478+
}
479+
480+
static bool IsDuplicateResult(GithubCodeSearchResult result, HashSet<string> duplicateSignatures)
481+
{
482+
const int maxTrackedSignatures = 2048;
483+
484+
if (duplicateSignatures.Count >= maxTrackedSignatures)
485+
{
486+
return false;
487+
}
488+
489+
var signature = CreateResultDuplicateSignature(result);
490+
491+
return !string.IsNullOrWhiteSpace(signature) && !duplicateSignatures.Add(signature);
492+
}
493+
415494
static void WriteLine(string value, TextWriter? outputWriter, ConsoleStatusLine statusLine)
416495
{
417496
statusLine.Clear();
@@ -485,7 +564,10 @@ static async Task WriteAdditionalLeadsAsync(GithubClient client, IReadOnlyList<A
485564
};
486565

487566
WriteColoredLine($" • {verdictLabel}: {url}", GetAiValidationColor(item.Verdict), outputWriter, statusLine);
488-
WriteLabeledLine(" Why", item.Reason, outputWriter, statusLine, ConsoleColor.Gray);
567+
if (HasMeaningfulSensitiveItemReason(item.Reason))
568+
{
569+
WriteLabeledLine(" Why", item.Reason, outputWriter, statusLine, ConsoleColor.Gray);
570+
}
489571

490572
if (!string.IsNullOrWhiteSpace(item.Snippet))
491573
{
@@ -494,6 +576,13 @@ static async Task WriteAdditionalLeadsAsync(GithubClient client, IReadOnlyList<A
494576
}
495577
}
496578

579+
static bool HasMeaningfulSensitiveItemReason(string? reason)
580+
{
581+
return !string.IsNullOrWhiteSpace(reason)
582+
&& !string.Equals(reason.Trim(), "Agent marked this item as sensitive.", StringComparison.OrdinalIgnoreCase)
583+
&& !reason.StartsWith("No item-specific reason was returned", StringComparison.OrdinalIgnoreCase);
584+
}
585+
497586
static void WriteSectionLine(string title, TextWriter? outputWriter, ConsoleStatusLine statusLine, ConsoleColor color)
498587
{
499588
WriteColoredLine(title, color, outputWriter, statusLine);
@@ -602,8 +691,12 @@ static string CreateRepositoryFileUrl(GithubCodeSearchRepository repository, str
602691
var repositoryUrl = !string.IsNullOrWhiteSpace(repository.HtmlUrl)
603692
? repository.HtmlUrl
604693
: $"https://github.com/{repository.FullName}";
694+
var branch = !string.IsNullOrWhiteSpace(repository.DefaultBranch)
695+
? repository.DefaultBranch
696+
: "main";
697+
var encodedBranch = string.Join('/', branch.Split('/', StringSplitOptions.RemoveEmptyEntries).Select(Uri.EscapeDataString));
605698
var encodedPath = string.Join('/', path.Split('/', StringSplitOptions.RemoveEmptyEntries).Select(Uri.EscapeDataString));
606-
return AppendLineAnchor($"{repositoryUrl.TrimEnd('/')}/blob/HEAD/{encodedPath}", lineNumber);
699+
return AppendLineAnchor($"{repositoryUrl.TrimEnd('/')}/blob/{encodedBranch}/{encodedPath}", lineNumber);
607700
}
608701

609702
static string AppendLineAnchor(string url, int? lineNumber)

GitRekt/Properties/launchSettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"profiles": {
33
"GitRekt": {
44
"commandName": "Project",
5-
"commandLineArgs": "--ai --ai-agent --ai-model \"gemma4:31b-cloud\" --ai-verdict yellow --github-app-id 3562299 --github-app-installation-id 128570455 -q \"@example.com\""
5+
"commandLineArgs": "--ai --ai-agent --ai-model \"gemma4:31b-cloud\" --ai-verdict possible --github-app-id 3562299 --github-app-installation-id 128570455 --strict -q \"@example.com\""
66
}
77
}
88
}

0 commit comments

Comments
 (0)