@shakecodeslikecray/whiterose 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -6,6 +6,7 @@ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, statSy
6
6
  import { join, isAbsolute, resolve, basename, relative, dirname } from 'path';
7
7
  import { execa } from 'execa';
8
8
  import { homedir, tmpdir } from 'os';
9
+ import { spawn } from 'child_process';
9
10
  import { z } from 'zod';
10
11
  import fg3 from 'fast-glob';
11
12
  import { createHash } from 'crypto';
@@ -14,6 +15,7 @@ import { render, useApp, useInput, Box, Text } from 'ink';
14
15
  import { useState, useEffect } from 'react';
15
16
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
16
17
  import Spinner from 'ink-spinner';
18
+ import * as readline from 'readline';
17
19
 
18
20
  var providerChecks = [
19
21
  {
@@ -583,7 +585,7 @@ var ClaudeCodeProvider = class {
583
585
  // Agentic Prompts
584
586
  // ─────────────────────────────────────────────────────────────
585
587
  buildAgenticAnalysisPrompt(understanding) {
586
- return `You are whiterose, an expert bug hunter. Your task is to explore this codebase and find real bugs.
588
+ return `You are whiterose, an expert bug hunter. Your task is to explore this codebase and find REAL bugs.
587
589
 
588
590
  CODEBASE CONTEXT:
589
591
  - Type: ${understanding.summary.type}
@@ -600,66 +602,78 @@ YOUR TASK:
600
602
  - Edge cases (empty arrays, zero values, boundaries)
601
603
  - Resource leaks (unclosed connections)
602
604
 
605
+ CRITICAL - INLINE VALIDATION:
606
+ Before reporting ANY bug, you MUST try to DISPROVE it:
607
+ 1. Check if there's a guard/validation that prevents the issue
608
+ 2. Check if the type system guarantees safety
609
+ 3. Check if framework behavior handles this case
610
+ 4. Check if the code path is actually reachable
611
+ 5. Only report the bug if you CANNOT disprove it
612
+
603
613
  PROTOCOL - You MUST output these markers:
604
- - Before reading each file, output: ${MARKERS.SCANNING}<filepath>
605
- - When you find a bug, output: ${MARKERS.BUG}<json>
614
+ - When reading a file, output: ${MARKERS.SCANNING}<filepath>
615
+ - When you find a VALIDATED bug, output: ${MARKERS.BUG}<json>
606
616
  - When completely done, output: ${MARKERS.COMPLETE}
607
617
  - If you encounter an error, output: ${MARKERS.ERROR}<message>
608
618
 
609
- BUG JSON FORMAT:
610
- ${MARKERS.BUG}{"file":"src/api/users.ts","line":42,"title":"Null dereference in getUserById","description":"...","severity":"high","category":"null-reference","evidence":["..."],"suggestedFix":"..."}
619
+ BUG JSON FORMAT (only after validation):
620
+ ${MARKERS.BUG}{"file":"src/api/users.ts","line":42,"title":"Null dereference in getUserById","description":"...","severity":"high","category":"null-reference","confidence":"high","evidence":["line of code","why it's reachable"],"validationNotes":"Checked for guards - none found. Type allows null.","suggestedFix":"..."}
621
+
622
+ SEVERITY GUIDE:
623
+ - critical: Security vulnerabilities, data loss, crashes in production
624
+ - high: Bugs that will definitely cause issues for users
625
+ - medium: Edge cases that may cause issues under certain conditions
626
+ - low: Minor issues, potential improvements
611
627
 
612
628
  IMPORTANT:
613
- - Only report bugs you have HIGH confidence in
614
- - Include exact line numbers
615
- - Focus on real bugs, not style issues
616
- - Explore systematically - check API routes, data handling, auth flows
629
+ - Do NOT report style issues or potential improvements
630
+ - Do NOT report bugs you haven't validated
631
+ - Include EXACT line numbers from the files you read
632
+ - Include validation notes explaining why the bug is real
633
+ - Explore systematically - check entry points, API routes, data flows
617
634
 
618
- Now explore this codebase and find bugs. Start by reading the main entry points.`;
635
+ Now explore this codebase and find VALIDATED bugs. Start by reading the main entry points.`;
619
636
  }
620
637
  buildAgenticUnderstandingPrompt(existingDocsSummary) {
621
638
  const docsSection = existingDocsSummary ? `
622
639
 
623
- EXISTING DOCUMENTATION (merge this with your exploration):
640
+ EXISTING DOCUMENTATION:
624
641
  ${existingDocsSummary}
625
642
  ` : "";
626
- return `You are whiterose. Your task is to understand this codebase.
643
+ return `You are whiterose. Your task is to QUICKLY understand this codebase structure.
627
644
  ${docsSection}
628
- YOUR TASK:
629
- 1. Review the existing documentation above (if any)
630
- 2. Explore the codebase structure to fill in gaps
631
- 3. Read key files (main entry points, config files, core modules)
632
- 4. Build a comprehensive understanding merging docs + code exploration
633
- 5. Identify main features, business rules, and behavioral contracts
645
+ YOUR TASK (be fast, read only key files):
646
+ 1. Read package.json or similar config to understand the project type
647
+ 2. Read the main entry point (index.ts, main.ts, app.ts, etc.)
648
+ 3. Skim 2-3 core files to understand the architecture
649
+ 4. Output your understanding - don't over-explore
634
650
 
635
651
  PROTOCOL - You MUST output these markers:
636
- - Before reading each file, output: ${MARKERS.SCANNING}<filepath>
637
- - When you have full understanding, output: ${MARKERS.UNDERSTANDING}<json>
638
- - When completely done, output: ${MARKERS.COMPLETE}
652
+ - When reading a file, output: ${MARKERS.SCANNING}<filepath>
653
+ - When you have understanding, output: ${MARKERS.UNDERSTANDING}<json>
654
+ - When done, output: ${MARKERS.COMPLETE}
639
655
 
640
656
  UNDERSTANDING JSON FORMAT:
641
657
  ${MARKERS.UNDERSTANDING}{
642
658
  "summary": {
643
- "type": "api|web-app|cli|library|etc",
644
- "framework": "next.js|express|react|etc",
659
+ "type": "api|web-app|cli|library|monorepo",
660
+ "framework": "next.js|express|react|fastify|none",
645
661
  "language": "typescript|javascript",
646
- "description": "2-3 sentence description"
662
+ "description": "1-2 sentence description of what this project does"
647
663
  },
648
664
  "features": [
649
- {"name": "Feature", "description": "What it does", "priority": "critical|high|medium|low", "constraints": ["business rule 1", "invariant 2"], "relatedFiles": ["path/to/file.ts"]}
665
+ {"name": "Feature Name", "description": "Brief description", "priority": "critical|high|medium|low", "relatedFiles": ["path/to/file.ts"]}
650
666
  ],
651
- "contracts": [
652
- {"function": "functionName", "file": "path/to/file.ts", "inputs": [], "outputs": {}, "invariants": ["must do X before Y"], "sideEffects": [], "throws": []}
653
- ]
667
+ "contracts": []
654
668
  }
655
669
 
656
670
  IMPORTANT:
657
- - Merge existing documentation with what you discover in the code
658
- - Focus on business rules and invariants (what MUST be true)
659
- - Identify critical paths (checkout, auth, payments, etc.)
660
- - Document behavioral contracts for important functions
671
+ - Be FAST - read 3-5 files max
672
+ - Focus on WHAT the project does, not implementation details
673
+ - Identify the main features/capabilities
674
+ - Don't deep-dive into every file
661
675
 
662
- Now explore this codebase and build understanding.`;
676
+ Now QUICKLY understand this codebase.`;
663
677
  }
664
678
  buildAdversarialPrompt(bug, fileContent) {
665
679
  return `You are a skeptical code reviewer. Try to DISPROVE this bug report.
@@ -688,26 +702,37 @@ Set "survived": true if you CANNOT disprove it (it's a real bug).`;
688
702
  // Claude CLI Execution (Agentic Mode)
689
703
  // ─────────────────────────────────────────────────────────────
690
704
  async runAgenticClaude(prompt, cwd, callbacks) {
705
+ this.streamBuffer = "";
706
+ this.fullResponseBuffer = "";
691
707
  const claudeCommand = getProviderCommand("claude-code");
692
- const args = ["--verbose", "-p", prompt];
693
- if (this.unsafeMode) {
694
- args.unshift("--dangerously-skip-permissions");
695
- }
696
- this.currentProcess = execa(
697
- claudeCommand,
698
- args,
699
- {
700
- cwd,
701
- env: {
702
- ...process.env,
703
- NO_COLOR: "1"
704
- },
705
- reject: false
708
+ const args = [
709
+ "-p",
710
+ prompt,
711
+ "--output-format",
712
+ "stream-json",
713
+ "--verbose"
714
+ ];
715
+ args.push("--dangerously-skip-permissions");
716
+ this.currentProcess = spawn(claudeCommand, args, {
717
+ cwd,
718
+ env: {
719
+ ...process.env,
720
+ NO_COLOR: "1"
721
+ },
722
+ stdio: ["ignore", "pipe", "pipe"]
723
+ });
724
+ let lastActivity = Date.now();
725
+ const heartbeat = setInterval(() => {
726
+ const elapsed = Math.floor((Date.now() - lastActivity) / 1e3);
727
+ if (elapsed > 10) {
728
+ this.reportProgress(`Analyzing... (${elapsed}s since last update)`);
706
729
  }
707
- );
730
+ }, 5e3);
708
731
  let buffer = "";
709
732
  this.currentProcess.stdout?.on("data", (chunk) => {
710
- buffer += chunk.toString();
733
+ lastActivity = Date.now();
734
+ const text3 = chunk.toString();
735
+ buffer += text3;
711
736
  const lines = buffer.split("\n");
712
737
  buffer = lines.pop() || "";
713
738
  for (const line of lines) {
@@ -715,17 +740,95 @@ Set "survived": true if you CANNOT disprove it (it's a real bug).`;
715
740
  }
716
741
  });
717
742
  this.currentProcess.stderr?.on("data", (chunk) => {
718
- const text4 = chunk.toString().trim();
719
- if (text4 && !text4.includes("Loading")) ;
743
+ lastActivity = Date.now();
744
+ const text3 = chunk.toString().trim();
745
+ if (text3) {
746
+ this.reportProgress(`Claude: ${text3.slice(0, 50)}...`);
747
+ }
720
748
  });
721
- await this.currentProcess;
749
+ await new Promise((resolve5, reject) => {
750
+ const timeout = setTimeout(() => {
751
+ this.currentProcess?.kill();
752
+ reject(new Error("Claude analysis timed out after 5 minutes"));
753
+ }, 3e5);
754
+ this.currentProcess?.on("exit", (code) => {
755
+ clearTimeout(timeout);
756
+ if (code !== 0 && code !== null) {
757
+ this.reportProgress(`Claude exited with code ${code}`);
758
+ }
759
+ resolve5();
760
+ });
761
+ this.currentProcess?.on("error", (err) => {
762
+ clearTimeout(timeout);
763
+ reject(err);
764
+ });
765
+ });
766
+ clearInterval(heartbeat);
722
767
  if (buffer.trim()) {
723
768
  this.processAgentOutput(buffer, callbacks);
724
769
  }
770
+ if (this.fullResponseBuffer && callbacks.onUnderstanding) {
771
+ this.tryExtractUnderstandingJson(this.fullResponseBuffer, callbacks);
772
+ }
725
773
  this.currentProcess = void 0;
726
774
  }
727
775
  processAgentOutput(line, callbacks) {
728
776
  const trimmed = line.trim();
777
+ if (!trimmed) return;
778
+ try {
779
+ const event = JSON.parse(trimmed);
780
+ if (event.type === "system") {
781
+ this.reportProgress("Claude initialized...");
782
+ } else if (event.type === "stream_event") {
783
+ const innerEvent = event.event;
784
+ if (innerEvent?.type === "content_block_delta") {
785
+ const text3 = innerEvent.delta?.text;
786
+ if (text3) {
787
+ this.processTextContent(text3, callbacks);
788
+ }
789
+ } else if (innerEvent?.type === "content_block_start") {
790
+ if (innerEvent.content_block?.type === "tool_use") {
791
+ const toolName = innerEvent.content_block.name || "tool";
792
+ this.reportProgress(`Using ${toolName}...`);
793
+ }
794
+ }
795
+ } else if (event.type === "assistant") {
796
+ const content = event.message?.content || [];
797
+ for (const item of content) {
798
+ if (item.type === "text" && item.text) {
799
+ this.processTextContent(item.text, callbacks);
800
+ } else if (item.type === "tool_use") {
801
+ const toolName = item.name || "tool";
802
+ const input = item.input || {};
803
+ if (toolName === "Read" && input.file_path) {
804
+ callbacks.onScanning?.(input.file_path);
805
+ this.reportProgress(`Reading: ${input.file_path.split("/").pop()}`);
806
+ } else if (toolName === "Glob" || toolName === "Grep") {
807
+ this.reportProgress(`Searching: ${input.pattern || "..."}`);
808
+ } else {
809
+ this.reportProgress(`Using ${toolName}...`);
810
+ }
811
+ }
812
+ }
813
+ } else if (event.type === "user") {
814
+ if (event.tool_use_result) {
815
+ const numFiles = event.tool_use_result.numFiles;
816
+ if (numFiles) {
817
+ this.reportProgress(`Found ${numFiles} files...`);
818
+ }
819
+ }
820
+ } else if (event.type === "result") {
821
+ if (event.result) {
822
+ this.processTextContent(event.result, callbacks);
823
+ this.tryExtractUnderstandingJson(event.result, callbacks);
824
+ }
825
+ callbacks.onComplete?.();
826
+ } else if (event.type === "error") {
827
+ callbacks.onError?.(event.error || event.message || "Unknown error");
828
+ }
829
+ return;
830
+ } catch {
831
+ }
729
832
  if (trimmed.startsWith(MARKERS.SCANNING)) {
730
833
  const file = trimmed.slice(MARKERS.SCANNING.length).trim();
731
834
  callbacks.onScanning?.(file);
@@ -742,6 +845,92 @@ Set "survived": true if you CANNOT disprove it (it's a real bug).`;
742
845
  callbacks.onError?.(error);
743
846
  }
744
847
  }
848
+ // Accumulated text for marker detection in streaming responses
849
+ streamBuffer = "";
850
+ // Full response text for fallback extraction
851
+ fullResponseBuffer = "";
852
+ processTextContent(text3, callbacks) {
853
+ this.streamBuffer += text3;
854
+ this.fullResponseBuffer += text3;
855
+ const lines = this.streamBuffer.split("\n");
856
+ this.streamBuffer = lines.pop() || "";
857
+ for (const line of lines) {
858
+ const trimmed = line.trim();
859
+ if (trimmed.startsWith(MARKERS.SCANNING)) {
860
+ callbacks.onScanning?.(trimmed.slice(MARKERS.SCANNING.length).trim());
861
+ } else if (trimmed.startsWith(MARKERS.BUG)) {
862
+ callbacks.onBugFound?.(trimmed.slice(MARKERS.BUG.length).trim());
863
+ } else if (trimmed.startsWith(MARKERS.UNDERSTANDING)) {
864
+ callbacks.onUnderstanding?.(trimmed.slice(MARKERS.UNDERSTANDING.length).trim());
865
+ } else if (trimmed.startsWith(MARKERS.COMPLETE)) {
866
+ callbacks.onComplete?.();
867
+ } else if (trimmed.startsWith(MARKERS.ERROR)) {
868
+ callbacks.onError?.(trimmed.slice(MARKERS.ERROR.length).trim());
869
+ }
870
+ }
871
+ }
872
+ // Try to extract understanding JSON from text without markers
873
+ tryExtractUnderstandingJson(text3, callbacks) {
874
+ const codeBlockMatch = text3.match(/```(?:json)?\s*([\s\S]*?)```/);
875
+ if (codeBlockMatch) {
876
+ const jsonStr = codeBlockMatch[1].trim();
877
+ try {
878
+ const parsed = JSON.parse(jsonStr);
879
+ if (parsed.summary && (parsed.summary.type || parsed.summary.language)) {
880
+ callbacks.onUnderstanding?.(jsonStr);
881
+ return;
882
+ }
883
+ } catch {
884
+ }
885
+ }
886
+ const candidates = [];
887
+ let depth = 0;
888
+ let start = -1;
889
+ let inString = false;
890
+ let escapeNext = false;
891
+ for (let i = 0; i < text3.length; i++) {
892
+ const char = text3[i];
893
+ if (escapeNext) {
894
+ escapeNext = false;
895
+ continue;
896
+ }
897
+ if (char === "\\" && inString) {
898
+ escapeNext = true;
899
+ continue;
900
+ }
901
+ if (char === '"' && !escapeNext) {
902
+ inString = !inString;
903
+ continue;
904
+ }
905
+ if (inString) continue;
906
+ if (char === "{") {
907
+ if (depth === 0) {
908
+ start = i;
909
+ }
910
+ depth++;
911
+ } else if (char === "}") {
912
+ depth--;
913
+ if (depth === 0 && start !== -1) {
914
+ const candidate = text3.slice(start, i + 1);
915
+ if (candidate.includes('"summary"') && candidate.includes('"type"')) {
916
+ candidates.push(candidate);
917
+ }
918
+ start = -1;
919
+ }
920
+ }
921
+ }
922
+ candidates.sort((a, b) => b.length - a.length);
923
+ for (const jsonStr of candidates) {
924
+ try {
925
+ const parsed = JSON.parse(jsonStr);
926
+ if (parsed.summary && (parsed.summary.type || parsed.summary.language)) {
927
+ callbacks.onUnderstanding?.(jsonStr);
928
+ return;
929
+ }
930
+ } catch {
931
+ }
932
+ }
933
+ }
745
934
  // Simple non-agentic mode for short prompts (adversarial validation)
746
935
  async runSimpleClaude(prompt, cwd) {
747
936
  const claudeCommand = getProviderCommand("claude-code");
@@ -903,18 +1092,63 @@ Set "survived": true if you CANNOT disprove it (it's a real bug).`;
903
1092
  // ─────────────────────────────────────────────────────────────
904
1093
  // Utilities
905
1094
  // ─────────────────────────────────────────────────────────────
906
- extractJson(text4) {
907
- const codeBlockMatch = text4.match(/```(?:json)?\s*([\s\S]*?)```/);
1095
+ extractJson(text3) {
1096
+ const codeBlockMatch = text3.match(/```(?:json)?\s*([\s\S]*?)```/);
908
1097
  if (codeBlockMatch) {
909
1098
  return codeBlockMatch[1].trim();
910
1099
  }
911
- const arrayMatch = text4.match(/\[[\s\S]*\]/);
912
- if (arrayMatch) {
913
- return arrayMatch[0];
1100
+ return this.findBalancedJson(text3);
1101
+ }
1102
+ // Find the first balanced JSON object or array in text
1103
+ findBalancedJson(text3) {
1104
+ const objectStart = text3.indexOf("{");
1105
+ const arrayStart = text3.indexOf("[");
1106
+ let start = -1;
1107
+ let openChar = "{";
1108
+ let closeChar = "}";
1109
+ if (objectStart === -1 && arrayStart === -1) {
1110
+ return null;
1111
+ } else if (objectStart === -1) {
1112
+ start = arrayStart;
1113
+ openChar = "[";
1114
+ closeChar = "]";
1115
+ } else if (arrayStart === -1) {
1116
+ start = objectStart;
1117
+ } else {
1118
+ if (arrayStart < objectStart) {
1119
+ start = arrayStart;
1120
+ openChar = "[";
1121
+ closeChar = "]";
1122
+ } else {
1123
+ start = objectStart;
1124
+ }
914
1125
  }
915
- const objectMatch = text4.match(/\{[\s\S]*\}/);
916
- if (objectMatch) {
917
- return objectMatch[0];
1126
+ let depth = 0;
1127
+ let inString = false;
1128
+ let escapeNext = false;
1129
+ for (let i = start; i < text3.length; i++) {
1130
+ const char = text3[i];
1131
+ if (escapeNext) {
1132
+ escapeNext = false;
1133
+ continue;
1134
+ }
1135
+ if (char === "\\" && inString) {
1136
+ escapeNext = true;
1137
+ continue;
1138
+ }
1139
+ if (char === '"' && !escapeNext) {
1140
+ inString = !inString;
1141
+ continue;
1142
+ }
1143
+ if (inString) continue;
1144
+ if (char === openChar) {
1145
+ depth++;
1146
+ } else if (char === closeChar) {
1147
+ depth--;
1148
+ if (depth === 0) {
1149
+ return text3.slice(start, i + 1);
1150
+ }
1151
+ }
918
1152
  }
919
1153
  return null;
920
1154
  }
@@ -1266,12 +1500,12 @@ Output JSON ONLY describing:
1266
1500
  // ─────────────────────────────────────────────────────────────
1267
1501
  // Utilities
1268
1502
  // ─────────────────────────────────────────────────────────────
1269
- extractJson(text4) {
1270
- const codeBlockMatch = text4.match(/```(?:json)?\s*([\s\S]*?)```/);
1503
+ extractJson(text3) {
1504
+ const codeBlockMatch = text3.match(/```(?:json)?\s*([\s\S]*?)```/);
1271
1505
  if (codeBlockMatch) return codeBlockMatch[1].trim();
1272
- const arrayMatch = text4.match(/\[[\s\S]*\]/);
1506
+ const arrayMatch = text3.match(/\[[\s\S]*\]/);
1273
1507
  if (arrayMatch) return arrayMatch[0];
1274
- const objectMatch = text4.match(/\{[\s\S]*\}/);
1508
+ const objectMatch = text3.match(/\{[\s\S]*\}/);
1275
1509
  if (objectMatch) return objectMatch[0];
1276
1510
  return null;
1277
1511
  }
@@ -1734,35 +1968,44 @@ async function initCommand(options) {
1734
1968
  process.exit(1);
1735
1969
  }
1736
1970
  p3.intro(chalk.red("whiterose") + chalk.dim(" - initialization"));
1737
- const providerSpinner = p3.spinner();
1738
- providerSpinner.start("Detecting available LLM providers...");
1739
- const availableProviders = await detectProvider();
1740
- if (availableProviders.length === 0) {
1741
- providerSpinner.stop("No LLM providers detected");
1742
- p3.log.error("whiterose requires an LLM provider to function.");
1743
- p3.log.info("Supported providers: claude-code, aider, codex, opencode");
1744
- p3.log.info("Install one and ensure it's configured, then run init again.");
1745
- process.exit(1);
1746
- }
1747
- providerSpinner.stop(`Detected providers: ${availableProviders.join(", ")}`);
1748
1971
  let selectedProvider;
1749
- if (options.skipQuestions) {
1750
- selectedProvider = availableProviders[0];
1751
- p3.log.info(`Using ${selectedProvider} as your LLM provider.`);
1972
+ if (options.skipProviderDetection && options.provider) {
1973
+ selectedProvider = options.provider;
1752
1974
  } else {
1753
- const providerChoice = await p3.select({
1754
- message: "Which LLM provider should whiterose use?",
1755
- options: availableProviders.map((prov) => ({
1756
- value: prov,
1757
- label: prov,
1758
- hint: prov === "claude-code" ? "recommended" : void 0
1759
- }))
1760
- });
1761
- if (p3.isCancel(providerChoice)) {
1762
- p3.cancel("Initialization cancelled.");
1763
- process.exit(0);
1975
+ const providerSpinner = p3.spinner();
1976
+ providerSpinner.start("Detecting available LLM providers...");
1977
+ const availableProviders = await detectProvider();
1978
+ if (availableProviders.length === 0) {
1979
+ providerSpinner.stop("No LLM providers detected");
1980
+ p3.log.error("whiterose requires an LLM provider to function.");
1981
+ p3.log.info("Supported providers: claude-code, aider, codex, opencode");
1982
+ p3.log.info("Install one and ensure it's configured, then run init again.");
1983
+ process.exit(1);
1984
+ }
1985
+ providerSpinner.stop(`Detected providers: ${availableProviders.join(", ")}`);
1986
+ const passedProvider = options.provider;
1987
+ const isPassedProviderAvailable = availableProviders.includes(passedProvider);
1988
+ if (isPassedProviderAvailable) {
1989
+ selectedProvider = passedProvider;
1990
+ p3.log.info(`Using ${selectedProvider} as your LLM provider.`);
1991
+ } else if (options.skipQuestions) {
1992
+ selectedProvider = availableProviders[0];
1993
+ p3.log.info(`Using ${selectedProvider} as your LLM provider.`);
1994
+ } else {
1995
+ const providerChoice = await p3.select({
1996
+ message: "Which LLM provider should whiterose use?",
1997
+ options: availableProviders.map((prov) => ({
1998
+ value: prov,
1999
+ label: prov,
2000
+ hint: prov === "claude-code" ? "recommended" : void 0
2001
+ }))
2002
+ });
2003
+ if (p3.isCancel(providerChoice)) {
2004
+ p3.cancel("Initialization cancelled.");
2005
+ process.exit(0);
2006
+ }
2007
+ selectedProvider = providerChoice;
1764
2008
  }
1765
- selectedProvider = providerChoice;
1766
2009
  }
1767
2010
  const verifySpinner = p3.spinner();
1768
2011
  verifySpinner.start("Verifying provider CLI works...");
@@ -1820,10 +2063,6 @@ async function initCommand(options) {
1820
2063
  let understanding;
1821
2064
  try {
1822
2065
  const provider = await getProvider(selectedProvider);
1823
- if (options.unsafe && "setUnsafeMode" in provider) {
1824
- provider.setUnsafeMode(true);
1825
- p3.log.warn("Running in unsafe mode (--unsafe). LLM permission prompts are bypassed.");
1826
- }
1827
2066
  if ("setProgressCallback" in provider) {
1828
2067
  provider.setProgressCallback((message) => {
1829
2068
  understandingSpinner.message(message);
@@ -2364,14 +2603,6 @@ async function scanCommand(paths, options) {
2364
2603
  }
2365
2604
  const providerName = options.provider || config.provider;
2366
2605
  const provider = await getProvider(providerName);
2367
- if (options.unsafe) {
2368
- if ("setUnsafeMode" in provider) {
2369
- provider.setUnsafeMode(true);
2370
- if (!isQuiet) {
2371
- p3.log.warn("Running in unsafe mode (--unsafe). LLM permission prompts are bypassed.");
2372
- }
2373
- }
2374
- }
2375
2606
  let bugs;
2376
2607
  if (!isQuiet) {
2377
2608
  const llmSpinner = p3.spinner();
@@ -2408,47 +2639,6 @@ async function scanCommand(paths, options) {
2408
2639
  staticAnalysisResults: staticResults
2409
2640
  });
2410
2641
  }
2411
- if (options.adversarial && bugs.length > 0) {
2412
- if (!isQuiet) {
2413
- const advSpinner = p3.spinner();
2414
- advSpinner.start("Running adversarial validation...");
2415
- const validatedBugs = [];
2416
- for (const bug of bugs) {
2417
- const result2 = await provider.adversarialValidate(bug, {
2418
- files: filesToScan,
2419
- understanding,
2420
- config,
2421
- staticAnalysisResults: staticResults
2422
- });
2423
- if (result2.survived) {
2424
- validatedBugs.push({
2425
- ...bug,
2426
- confidence: result2.adjustedConfidence || bug.confidence
2427
- });
2428
- }
2429
- }
2430
- const filtered = bugs.length - validatedBugs.length;
2431
- advSpinner.stop(`Adversarial validation: ${filtered} false positives filtered`);
2432
- bugs = validatedBugs;
2433
- } else {
2434
- const validatedBugs = [];
2435
- for (const bug of bugs) {
2436
- const result2 = await provider.adversarialValidate(bug, {
2437
- files: filesToScan,
2438
- understanding,
2439
- config,
2440
- staticAnalysisResults: staticResults
2441
- });
2442
- if (result2.survived) {
2443
- validatedBugs.push({
2444
- ...bug,
2445
- confidence: result2.adjustedConfidence || bug.confidence
2446
- });
2447
- }
2448
- }
2449
- bugs = validatedBugs;
2450
- }
2451
- }
2452
2642
  const minConfidence = options.minConfidence;
2453
2643
  const confidenceOrder = { high: 3, medium: 2, low: 1 };
2454
2644
  bugs = bugs.filter((bug) => confidenceOrder[bug.confidence.overall] >= confidenceOrder[minConfidence]);
@@ -3374,12 +3564,12 @@ function generateDiff(original, fixed, filename) {
3374
3564
  return diff.join("\n");
3375
3565
  }
3376
3566
  async function startFixTUI(bugs, config, options) {
3377
- return new Promise((resolve4) => {
3567
+ return new Promise((resolve5) => {
3378
3568
  const handleFix = async (bug) => {
3379
3569
  await applyFix(bug, config, options);
3380
3570
  };
3381
3571
  const handleExit = () => {
3382
- resolve4();
3572
+ resolve5();
3383
3573
  };
3384
3574
  const { unmount, waitUntilExit } = render(
3385
3575
  /* @__PURE__ */ jsx(
@@ -3394,7 +3584,7 @@ async function startFixTUI(bugs, config, options) {
3394
3584
  )
3395
3585
  );
3396
3586
  waitUntilExit().then(() => {
3397
- resolve4();
3587
+ resolve5();
3398
3588
  });
3399
3589
  });
3400
3590
  }
@@ -3936,6 +4126,160 @@ async function reportCommand(options) {
3936
4126
  p3.log.success(`Report written to ${options.output}`);
3937
4127
  }
3938
4128
  }
4129
+ function getPathCompletions(partial) {
4130
+ try {
4131
+ if (!partial) partial = ".";
4132
+ const resolved = resolve(partial);
4133
+ let dir;
4134
+ let prefix;
4135
+ if (partial.endsWith("/") || existsSync(resolved) && statSync(resolved).isDirectory()) {
4136
+ dir = resolved;
4137
+ prefix = "";
4138
+ } else {
4139
+ dir = dirname(resolved);
4140
+ prefix = basename(partial);
4141
+ }
4142
+ if (!existsSync(dir)) {
4143
+ return [];
4144
+ }
4145
+ const entries = readdirSync(dir, { withFileTypes: true });
4146
+ const matches = entries.filter((entry) => entry.isDirectory()).filter((entry) => entry.name.toLowerCase().startsWith(prefix.toLowerCase())).filter((entry) => !entry.name.startsWith(".") || prefix.startsWith(".")).map((entry) => {
4147
+ if (partial.endsWith("/") || prefix === "") {
4148
+ return partial + entry.name + "/";
4149
+ }
4150
+ const base = partial.slice(0, partial.length - prefix.length);
4151
+ return base + entry.name + "/";
4152
+ }).sort();
4153
+ return matches;
4154
+ } catch {
4155
+ return [];
4156
+ }
4157
+ }
4158
+ function longestCommonPrefix(strings) {
4159
+ if (strings.length === 0) return "";
4160
+ if (strings.length === 1) return strings[0];
4161
+ let prefix = strings[0];
4162
+ for (let i = 1; i < strings.length; i++) {
4163
+ while (!strings[i].startsWith(prefix)) {
4164
+ prefix = prefix.slice(0, -1);
4165
+ if (prefix === "") return "";
4166
+ }
4167
+ }
4168
+ return prefix;
4169
+ }
4170
+ function pathInput(options) {
4171
+ return new Promise((resolvePromise) => {
4172
+ const { message, defaultValue = "", validate } = options;
4173
+ const promptText = chalk.cyan("\u25C6") + " " + chalk.bold(message);
4174
+ const hint = defaultValue ? chalk.dim(` (default: ${defaultValue})`) : "";
4175
+ const tabHint = chalk.dim(" [Tab to autocomplete]");
4176
+ process.stdout.write(promptText + hint + tabHint + "\n");
4177
+ process.stdout.write(chalk.cyan("\u2502") + " ");
4178
+ let input = "";
4179
+ let cursorPos = 0;
4180
+ if (process.stdin.isTTY) {
4181
+ process.stdin.setRawMode(true);
4182
+ }
4183
+ process.stdin.resume();
4184
+ process.stdin.setEncoding("utf8");
4185
+ const redrawLine = () => {
4186
+ readline.clearLine(process.stdout, 0);
4187
+ readline.cursorTo(process.stdout, 0);
4188
+ process.stdout.write(chalk.cyan("\u2502") + " " + input);
4189
+ readline.cursorTo(process.stdout, 2 + cursorPos);
4190
+ };
4191
+ const cleanup = () => {
4192
+ if (process.stdin.isTTY) {
4193
+ process.stdin.setRawMode(false);
4194
+ }
4195
+ process.stdin.pause();
4196
+ process.stdin.removeAllListeners("data");
4197
+ };
4198
+ const finish = (result) => {
4199
+ cleanup();
4200
+ console.log();
4201
+ resolvePromise(result);
4202
+ };
4203
+ process.stdin.on("data", (key) => {
4204
+ const code = key.charCodeAt(0);
4205
+ if (code === 3) {
4206
+ console.log();
4207
+ finish(null);
4208
+ return;
4209
+ }
4210
+ if (code === 13) {
4211
+ const finalPath = input.trim() || defaultValue;
4212
+ if (validate) {
4213
+ const error = validate(finalPath);
4214
+ if (error) {
4215
+ console.log();
4216
+ process.stdout.write(chalk.red("\u2502") + " " + chalk.red(error) + "\n");
4217
+ process.stdout.write(chalk.cyan("\u2502") + " " + input);
4218
+ return;
4219
+ }
4220
+ }
4221
+ finish(finalPath);
4222
+ return;
4223
+ }
4224
+ if (code === 9) {
4225
+ const completions = getPathCompletions(input || ".");
4226
+ if (completions.length === 1) {
4227
+ input = completions[0];
4228
+ cursorPos = input.length;
4229
+ redrawLine();
4230
+ } else if (completions.length > 1) {
4231
+ const common = longestCommonPrefix(completions);
4232
+ if (common.length > input.length) {
4233
+ input = common;
4234
+ cursorPos = input.length;
4235
+ redrawLine();
4236
+ } else {
4237
+ console.log();
4238
+ const maxShow = 10;
4239
+ const shown = completions.slice(0, maxShow);
4240
+ process.stdout.write(chalk.dim("\u2502 ") + shown.map((c) => chalk.cyan(basename(c.slice(0, -1)))).join(" "));
4241
+ if (completions.length > maxShow) {
4242
+ process.stdout.write(chalk.dim(` ... +${completions.length - maxShow} more`));
4243
+ }
4244
+ console.log();
4245
+ process.stdout.write(chalk.cyan("\u2502") + " " + input);
4246
+ }
4247
+ }
4248
+ return;
4249
+ }
4250
+ if (code === 127 || code === 8) {
4251
+ if (cursorPos > 0) {
4252
+ input = input.slice(0, cursorPos - 1) + input.slice(cursorPos);
4253
+ cursorPos--;
4254
+ redrawLine();
4255
+ }
4256
+ return;
4257
+ }
4258
+ if (key === "\x1B[D") {
4259
+ if (cursorPos > 0) {
4260
+ cursorPos--;
4261
+ redrawLine();
4262
+ }
4263
+ return;
4264
+ }
4265
+ if (key === "\x1B[C") {
4266
+ if (cursorPos < input.length) {
4267
+ cursorPos++;
4268
+ redrawLine();
4269
+ }
4270
+ return;
4271
+ }
4272
+ if (key.startsWith("\x1B")) {
4273
+ return;
4274
+ }
4275
+ if (code >= 32 && code < 127) {
4276
+ input = input.slice(0, cursorPos) + key + input.slice(cursorPos);
4277
+ cursorPos++;
4278
+ redrawLine();
4279
+ }
4280
+ });
4281
+ });
4282
+ }
3939
4283
 
3940
4284
  // src/cli/index.ts
3941
4285
  var BANNER = `
@@ -3949,15 +4293,15 @@ ${chalk.red(" \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u255
3949
4293
  ${chalk.dim(` "I've been staring at your code for a long time."`)}
3950
4294
  `;
3951
4295
  var program = new Command();
3952
- program.name("whiterose").description("AI-powered bug hunter that uses your existing LLM subscription").version("0.2.0").hook("preAction", () => {
4296
+ program.name("whiterose").description("AI-powered bug hunter that uses your existing LLM subscription").version("0.2.3").hook("preAction", () => {
3953
4297
  const args = process.argv.slice(2);
3954
4298
  if (!args.includes("--help") && !args.includes("-h") && args.length > 0) {
3955
4299
  console.log(BANNER);
3956
4300
  }
3957
4301
  });
3958
- program.command("init").description("Initialize whiterose for this project (scans codebase, asks questions, generates config)").option("-p, --provider <provider>", "LLM provider to use", "claude-code").option("--skip-questions", "Skip interactive questions, use defaults").option("--force", "Overwrite existing .whiterose directory").option("--unsafe", "Bypass LLM permission prompts (use with caution)").action(initCommand);
3959
- program.command("scan [paths...]").description("Scan for bugs in the codebase").option("-f, --full", "Force full scan (ignore cache)").option("--json", "Output as JSON only").option("--sarif", "Output as SARIF only").option("-p, --provider <provider>", "Override LLM provider").option("-c, --category <categories...>", "Filter by bug categories").option("--min-confidence <level>", "Minimum confidence level to report", "low").option("--no-adversarial", "Skip adversarial validation (faster, less accurate)").option("--unsafe", "Bypass LLM permission prompts (use with caution)").action(scanCommand);
3960
- program.command("fix [bugId]").description("Fix bugs interactively or by ID").option("--dry-run", "Show proposed fixes without applying").option("--branch <name>", "Create fixes in a new branch").option("--sarif <path>", "Load bugs from an external SARIF file").option("--github <url>", "Load bug from a GitHub issue URL").option("--describe", "Manually describe a bug to fix").action(fixCommand);
4302
+ program.command("init").description("Initialize whiterose for this project (scans codebase, asks questions, generates config)").option("-p, --provider <provider>", "LLM provider to use", "claude-code").option("--skip-questions", "Skip interactive questions, use defaults").option("--force", "Overwrite existing .whiterose directory").action(initCommand);
4303
+ program.command("scan [paths...]").description("Scan for bugs in the codebase (includes inline validation)").option("-f, --full", "Force full scan (ignore cache)").option("--json", "Output as JSON only").option("--sarif", "Output as SARIF only").option("-p, --provider <provider>", "Override LLM provider").option("-c, --category <categories...>", "Filter by bug categories").option("--min-confidence <level>", "Minimum confidence level to report", "low").action(scanCommand);
4304
+ program.command("fix [bugId]").description("Fix bugs interactively or by ID").option("--dry-run", "Show proposed fixes without applying").option("--branch <name>", "Create fixes in a new branch").option("--sarif <path>", "Load bugs from an external SARIF file").option("--github <url>", "Load bug from a GitHub issue URL").option("--describe", "Manually describe a bug to fix").option("--unsafe", "Skip all permission prompts (full trust mode)").action(fixCommand);
3961
4305
  program.command("refresh").description("Rebuild codebase understanding from scratch").option("--keep-config", "Keep existing config, only regenerate understanding").action(refreshCommand);
3962
4306
  program.command("status").description("Show whiterose status (cache, last scan, provider)").action(statusCommand);
3963
4307
  program.command("report").description("Generate BUGS.md from last scan").option("-o, --output <path>", "Output path", "BUGS.md").option("--format <format>", "Output format (markdown, sarif, json)", "markdown").action(reportCommand);
@@ -3966,10 +4310,9 @@ async function showInteractiveWizard() {
3966
4310
  p3.intro(chalk.red("whiterose") + chalk.dim(" - AI Bug Hunter"));
3967
4311
  const cwd = process.cwd();
3968
4312
  const defaultPath = cwd;
3969
- const repoPath = await p3.text({
4313
+ const repoPath = await pathInput({
3970
4314
  message: "Repository path",
3971
- placeholder: defaultPath,
3972
- initialValue: defaultPath,
4315
+ defaultValue: defaultPath,
3973
4316
  validate: (value) => {
3974
4317
  const path = value || defaultPath;
3975
4318
  if (!existsSync(path)) {
@@ -3978,7 +4321,7 @@ async function showInteractiveWizard() {
3978
4321
  return void 0;
3979
4322
  }
3980
4323
  });
3981
- if (p3.isCancel(repoPath)) {
4324
+ if (repoPath === null) {
3982
4325
  p3.cancel("Cancelled.");
3983
4326
  process.exit(0);
3984
4327
  }
@@ -4034,7 +4377,9 @@ async function showInteractiveWizard() {
4034
4377
  provider: selectedProvider,
4035
4378
  skipQuestions: false,
4036
4379
  force: false,
4037
- unsafe: false
4380
+ // Read-only operations auto-approve, unsafe only matters for fix
4381
+ skipProviderDetection: true
4382
+ // Already verified in wizard
4038
4383
  });
4039
4384
  console.log();
4040
4385
  } else {
@@ -4069,9 +4414,7 @@ async function showInteractiveWizard() {
4069
4414
  sarif: false,
4070
4415
  provider: selectedProvider,
4071
4416
  category: void 0,
4072
- minConfidence: "low",
4073
- adversarial: true,
4074
- unsafe: false
4417
+ minConfidence: "low"
4075
4418
  });
4076
4419
  }
4077
4420
  if (process.argv.length === 2) {