vigthoria-cli 1.6.16 → 1.6.18

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.
@@ -8,8 +8,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.AuthCommand = void 0;
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
- const ora_1 = __importDefault(require("ora"));
12
11
  const inquirer_1 = __importDefault(require("inquirer"));
12
+ const logger_js_1 = require("../utils/logger.js");
13
13
  const api_js_1 = require("../utils/api.js");
14
14
  class AuthCommand {
15
15
  config;
@@ -73,7 +73,7 @@ class AuthCommand {
73
73
  validate: (input) => input.length >= 6 || 'Password must be at least 6 characters',
74
74
  },
75
75
  ]);
76
- const spinner = (0, ora_1.default)('Logging in...').start();
76
+ const spinner = (0, logger_js_1.createSpinner)('Logging in...').start();
77
77
  const success = await this.api.login(credentials.email, credentials.password);
78
78
  spinner.stop();
79
79
  if (success) {
@@ -96,7 +96,7 @@ class AuthCommand {
96
96
  await this.loginWithToken(token);
97
97
  }
98
98
  async loginWithToken(token) {
99
- const spinner = (0, ora_1.default)('Validating token...').start();
99
+ const spinner = (0, logger_js_1.createSpinner)('Validating token...').start();
100
100
  const success = await this.api.loginWithToken(token);
101
101
  spinner.stop();
102
102
  if (success) {
@@ -173,7 +173,7 @@ class AuthCommand {
173
173
  });
174
174
  console.log();
175
175
  // API status
176
- const spinner = (0, ora_1.default)('Checking API status...').start();
176
+ const spinner = (0, logger_js_1.createSpinner)('Checking API status...').start();
177
177
  const apiStatus = await this.api.getHealthStatus();
178
178
  spinner.stop();
179
179
  console.log(chalk_1.default.white('API Status:'));
@@ -198,7 +198,7 @@ class AuthCommand {
198
198
  else {
199
199
  console.log(chalk_1.default.gray(' Self-hosted Models: ') + chalk_1.default.gray('disabled'));
200
200
  }
201
- const capabilitySpinner = (0, ora_1.default)('Checking live capability truth...').start();
201
+ const capabilitySpinner = (0, logger_js_1.createSpinner)('Checking live capability truth...').start();
202
202
  const capabilityStatus = await this.api.getCapabilityTruthStatus({
203
203
  workspacePath: process.cwd(),
204
204
  projectPath: process.cwd(),
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.BridgeCommand = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
- const ora_1 = __importDefault(require("ora"));
8
+ const logger_js_1 = require("../utils/logger.js");
9
9
  const api_js_1 = require("../utils/api.js");
10
10
  class BridgeCommand {
11
11
  api;
@@ -13,7 +13,7 @@ class BridgeCommand {
13
13
  this.api = new api_js_1.APIClient(config, logger);
14
14
  }
15
15
  async status() {
16
- const spinner = (0, ora_1.default)('Checking DevTools Bridge...').start();
16
+ const spinner = (0, logger_js_1.createSpinner)('Checking DevTools Bridge...').start();
17
17
  const bridge = await this.api.getDevtoolsBridgeStatus();
18
18
  spinner.stop();
19
19
  console.log();
@@ -32,6 +32,7 @@ export declare class ChatCommand {
32
32
  private currentModel;
33
33
  private modelExplicitlySelected;
34
34
  private autoApprove;
35
+ private agentToolEvidence;
35
36
  private operatorMode;
36
37
  private workflowTarget;
37
38
  private savePlanToVigFlow;
@@ -38,11 +38,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.ChatCommand = void 0;
40
40
  const chalk_1 = __importDefault(require("chalk"));
41
- const ora_1 = __importDefault(require("ora"));
42
41
  const fs = __importStar(require("fs"));
43
42
  const os = __importStar(require("os"));
44
43
  const path = __importStar(require("path"));
45
44
  const readline = __importStar(require("readline"));
45
+ const logger_js_1 = require("../utils/logger.js");
46
46
  const api_js_1 = require("../utils/api.js");
47
47
  const tools_js_1 = require("../utils/tools.js");
48
48
  const session_js_1 = require("../utils/session.js");
@@ -67,6 +67,8 @@ class ChatCommand {
67
67
  currentModel = 'code';
68
68
  modelExplicitlySelected = false;
69
69
  autoApprove = false;
70
+ // Phase 5: Agent quality gate — track tool usage for evidence thresholds
71
+ agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
70
72
  operatorMode = false;
71
73
  workflowTarget = null;
72
74
  savePlanToVigFlow = false;
@@ -195,6 +197,10 @@ class ChatCommand {
195
197
  }
196
198
  buildTaskShapingInstructions(prompt) {
197
199
  const instructions = [];
200
+ // Platform-aware routing hints
201
+ if (os.platform() === 'win32') {
202
+ instructions.push('Platform: Windows. Use list_dir, glob, read_file, and the grep tool for searching.', 'The grep tool handles Windows automatically — do not use bash to call grep, findstr, or Select-String manually.', 'Do not use bash for Unix commands (cat, head, tail, awk, sed, wc).', 'Use read_file to inspect file contents instead of shell commands.', 'All file paths use forward slashes internally.');
203
+ }
198
204
  if (this.isDiagnosticPrompt(prompt)) {
199
205
  instructions.push('Diagnostic mode is active.', 'Treat this as a debugging task, not a generic code review or feature request.', 'Start with concrete evidence: logs, runtime errors, config, launch files, and exact symbol references.', 'If log files exist, inspect them before proposing fixes.', 'Do not claim a file, definition, asset, or symbol is missing until you verify that with tools.', 'If a prior diagnosis mentioned a missing symbol or YAML entry, re-check the actual files before repeating it.', 'Prefer grep plus read_file around the exact references involved in the failure.', 'Separate your reasoning into: Evidence, Confirmed Cause, and Remaining Hypotheses.', 'Do not suggest speculative fixes when the current evidence contradicts them.');
200
206
  }
@@ -537,7 +543,7 @@ class ChatCommand {
537
543
  const runtimeContext = await this.getPromptRuntimeContext(prompt);
538
544
  const resolvedWorkflow = await this.api.resolveVigFlowWorkflow(selector);
539
545
  const invocationMode = this.operatorMode ? 'operator' : this.agentMode ? 'agent' : 'chat';
540
- const spinner = this.jsonOutput ? null : (0, ora_1.default)({ text: `Running workflow ${resolvedWorkflow.name}...`, spinner: 'clock' }).start();
546
+ const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: `Running workflow ${resolvedWorkflow.name}...`, spinner: 'clock' }).start();
541
547
  try {
542
548
  const execution = await this.api.runVigFlowWorkflow(resolvedWorkflow.id, {
543
549
  data: {
@@ -606,7 +612,7 @@ class ChatCommand {
606
612
  return;
607
613
  }
608
614
  const runtimeContext = await this.getPromptRuntimeContext(prompt);
609
- const spinner = this.jsonOutput ? null : (0, ora_1.default)({ text: 'Thinking like an operator...', spinner: 'clock' }).start();
615
+ const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking like an operator...', spinner: 'clock' }).start();
610
616
  const executionPrompt = this.buildExecutionPrompt(prompt);
611
617
  const workflowType = this.isDiagnosticPrompt(prompt) ? 'analysis_only' : 'full_autonomy';
612
618
  try {
@@ -657,10 +663,11 @@ class ChatCommand {
657
663
  async runSimplePrompt(prompt) {
658
664
  this.lastActionableUserInput = prompt;
659
665
  this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
660
- const spinner = (0, ora_1.default)({ text: 'Thinking...', spinner: 'clock' }).start();
666
+ const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking...', spinner: 'clock' }).start();
661
667
  try {
662
668
  const response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
663
- spinner.stop();
669
+ if (spinner)
670
+ spinner.stop();
664
671
  const finalText = response.message.trim();
665
672
  if (finalText) {
666
673
  console.log(finalText);
@@ -669,7 +676,8 @@ class ChatCommand {
669
676
  this.saveSession();
670
677
  }
671
678
  catch (error) {
672
- spinner.fail('Failed to get response');
679
+ if (spinner)
680
+ spinner.fail('Failed to get response');
673
681
  this.logger.error(error.message);
674
682
  }
675
683
  }
@@ -707,21 +715,37 @@ class ChatCommand {
707
715
  }
708
716
  this.lastActionableUserInput = prompt;
709
717
  this.directToolContinuationCount = 0;
718
+ this.agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
710
719
  this.tools.clearSessionApprovals();
711
720
  this.ensureAgentSystemPrompt();
712
721
  this.messages.push({ role: 'user', content: this.buildScopedUserPrompt(prompt) });
713
722
  this.saveSession();
714
723
  const maxTurns = 10;
715
724
  for (let turn = 0; turn < maxTurns; turn += 1) {
716
- const spinner = (0, ora_1.default)({ text: turn === 0 ? 'Planning...' : 'Continuing...', spinner: 'clock' }).start();
725
+ const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: turn === 0 ? 'Planning...' : 'Continuing...', spinner: 'clock' }).start();
717
726
  try {
718
727
  const response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
719
- spinner.stop();
728
+ if (spinner)
729
+ spinner.stop();
720
730
  const assistantMessage = response.message || '';
721
731
  this.messages.push({ role: 'assistant', content: assistantMessage });
722
732
  const toolCalls = this.extractToolCalls(assistantMessage);
723
733
  const visibleText = this.stripToolPayloads(assistantMessage).trim();
724
734
  if (toolCalls.length === 0) {
735
+ // Phase 5: Quality gate — if the agent tries to conclude on the first
736
+ // turn without any discovery, push it to gather evidence first.
737
+ if (turn === 0 && this.agentToolEvidence.discovery === 0 && this.isDiagnosticPrompt(prompt)) {
738
+ this.messages.push({
739
+ role: 'system',
740
+ content: [
741
+ 'Quality gate: you concluded without using any discovery tools (list_dir, glob, read_file, grep).',
742
+ 'Before answering a diagnostic or audit question, you MUST inspect the project with tools.',
743
+ 'Use list_dir and read_file to gather concrete evidence, then provide your answer.',
744
+ ].join('\n'),
745
+ });
746
+ this.directToolContinuationCount += 1;
747
+ continue;
748
+ }
725
749
  const finalContent = this.resolveDirectModeCompletion(prompt, visibleText);
726
750
  if (this.jsonOutput) {
727
751
  console.log(JSON.stringify({
@@ -750,7 +774,8 @@ class ChatCommand {
750
774
  this.saveSession();
751
775
  }
752
776
  catch (error) {
753
- spinner.fail('Agent request failed');
777
+ if (spinner)
778
+ spinner.fail('Agent request failed');
754
779
  if (this.jsonOutput) {
755
780
  process.exitCode = 1;
756
781
  console.log(JSON.stringify({
@@ -910,7 +935,7 @@ class ChatCommand {
910
935
  this.v3IterationCount = 0;
911
936
  this.v3ToolCallCount = 0;
912
937
  this.v3LastActivity = Date.now();
913
- const spinner = this.jsonOutput ? null : (0, ora_1.default)({
938
+ const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({
914
939
  text: routingPolicy.cloudSelected ? 'Routing heavy task to Vigthoria Cloud...' : 'Routing to V3 Agent...',
915
940
  spinner: 'clock',
916
941
  }).start();
@@ -971,6 +996,18 @@ class ChatCommand {
971
996
  }
972
997
  this.logger.error(errorMessage);
973
998
  this.messages.push({ role: 'assistant', content: errorMessage });
999
+ if (this.jsonOutput) {
1000
+ process.exitCode = 1;
1001
+ console.log(JSON.stringify({
1002
+ success: false,
1003
+ mode: 'agent',
1004
+ model: routingPolicy.selectedModel,
1005
+ partial: false,
1006
+ content: '',
1007
+ error: errorMessage,
1008
+ metadata: { executionPath: 'v3-agent', previewGate },
1009
+ }, null, 2));
1010
+ }
974
1011
  return true;
975
1012
  }
976
1013
  if (!this.jsonOutput && previewGate?.required && previewGate?.passed !== true && workspaceHasOutput) {
@@ -1028,6 +1065,18 @@ class ChatCommand {
1028
1065
  const errorMessage = `Agent mode requires the V3 workflow and will not fall back to the legacy CLI loop. ${error.message}`;
1029
1066
  this.logger.error(errorMessage);
1030
1067
  this.messages.push({ role: 'assistant', content: errorMessage });
1068
+ if (this.jsonOutput) {
1069
+ process.exitCode = 1;
1070
+ console.log(JSON.stringify({
1071
+ success: false,
1072
+ mode: 'agent',
1073
+ model: routingPolicy.selectedModel,
1074
+ partial: false,
1075
+ content: '',
1076
+ error: errorMessage,
1077
+ metadata: { executionPath: 'v3-agent' },
1078
+ }, null, 2));
1079
+ }
1031
1080
  return true;
1032
1081
  }
1033
1082
  }
@@ -1437,10 +1486,20 @@ class ChatCommand {
1437
1486
  }
1438
1487
  buildContinuationPrompt() {
1439
1488
  const diagnosticMode = this.isDiagnosticPrompt(this.lastActionableUserInput);
1489
+ const { discovery, mutation, searchFailed } = this.agentToolEvidence;
1490
+ const evidenceLines = [];
1491
+ if (discovery < 2) {
1492
+ evidenceLines.push(`Quality gate: only ${discovery} discovery tool(s) used so far (list_dir, glob, read_file, grep). Use at least 2 before concluding.`);
1493
+ }
1494
+ if (searchFailed > 0) {
1495
+ evidenceLines.push(`Warning: ${searchFailed} search tool call(s) failed. Do not treat failed searches as evidence that something is missing.`);
1496
+ }
1440
1497
  return [
1441
1498
  `Tool results received for direct mode step ${this.directToolContinuationCount + 1}.`,
1442
1499
  `Original user request: ${this.lastActionableUserInput}`,
1443
1500
  `Project root boundary: ${this.currentProjectPath}`,
1501
+ `Evidence collected: ${discovery} discovery, ${mutation} mutation, ${searchFailed} search failures.`,
1502
+ ...evidenceLines,
1444
1503
  'Do not declare success until the exact user question has been answered with tool-backed evidence.',
1445
1504
  'If a user is asking which file is correct or most recent, keep inspecting until you can justify the answer from actual results.',
1446
1505
  diagnosticMode ? 'Because this is a debugging task, prefer logs, runtime evidence, and exact symbol references over generic fixes.' : 'Keep working from concrete tool results.',
@@ -1604,16 +1663,47 @@ class ChatCommand {
1604
1663
  if (!this.jsonOutput) {
1605
1664
  console.log(chalk_1.default.cyan(`⚙ Executing: ${call.tool}`));
1606
1665
  }
1607
- const result = await this.tools.execute(call);
1666
+ let result = await this.tools.execute(call);
1667
+ // Phase 2: If a search tool failed (search_failed), retry with alternate approach
1668
+ const searchStatus = result.metadata?.searchStatus;
1669
+ if (call.tool === 'grep' && searchStatus === 'search_failed') {
1670
+ if (!this.jsonOutput) {
1671
+ console.log(chalk_1.default.yellow(`⚠ Search backend failed, retrying with alternate method...`));
1672
+ }
1673
+ // Force Node-native fallback by re-executing with a note
1674
+ const fallbackResult = await this.tools.execute({
1675
+ tool: 'grep',
1676
+ args: { ...call.args, _fallback: 'node-native' },
1677
+ });
1678
+ if (fallbackResult.success || fallbackResult.metadata?.searchStatus !== 'search_failed') {
1679
+ result = fallbackResult;
1680
+ }
1681
+ }
1608
1682
  const summary = this.formatToolResult(call, result);
1609
1683
  if (!this.jsonOutput) {
1610
1684
  console.log(result.success ? chalk_1.default.gray(summary) : chalk_1.default.red(summary));
1611
1685
  }
1612
1686
  this.messages.push({ role: 'system', content: summary });
1687
+ // Phase 5: Track tool evidence for quality gates
1688
+ const finalStatus = result.metadata?.searchStatus;
1689
+ if (finalStatus === 'search_failed') {
1690
+ this.agentToolEvidence.searchFailed += 1;
1691
+ }
1692
+ else if (/^(read_file|list_dir|glob|grep|git|repo|fetch_url)$/.test(call.tool)) {
1693
+ this.agentToolEvidence.discovery += 1;
1694
+ }
1695
+ else if (/^(write_file|edit_file|bash|ssh_exec)$/.test(call.tool)) {
1696
+ this.agentToolEvidence.mutation += 1;
1697
+ }
1613
1698
  }
1614
1699
  }
1615
1700
  formatToolResult(call, result) {
1616
- const parts = [`Tool ${call.tool} ${result.success ? 'succeeded' : 'failed'}.`];
1701
+ const parts = [`Tool ${call.tool} ${result.success ? 'succeeded' : 'FAILED'}.`];
1702
+ // Include search status classification for the agent to reason about
1703
+ const searchStatus = result.metadata?.searchStatus;
1704
+ if (searchStatus) {
1705
+ parts.push(`Search status: ${searchStatus}`);
1706
+ }
1617
1707
  if (result.output) {
1618
1708
  parts.push(`Output:\n${this.truncateText(result.output)}`);
1619
1709
  }
@@ -53,7 +53,7 @@ exports.DeployCommand = void 0;
53
53
  const chalk_1 = __importDefault(require("chalk"));
54
54
  const fs = __importStar(require("fs"));
55
55
  const path = __importStar(require("path"));
56
- const ora_1 = __importDefault(require("ora"));
56
+ const logger_js_1 = require("../utils/logger.js");
57
57
  const inquirer_1 = __importDefault(require("inquirer"));
58
58
  class DeployCommand {
59
59
  config;
@@ -127,7 +127,7 @@ class DeployCommand {
127
127
  * Deploy to preview URL (free)
128
128
  */
129
129
  async deployToPreview(projectPath) {
130
- const spinner = (0, ora_1.default)('Deploying to preview...').start();
130
+ const spinner = (0, logger_js_1.createSpinner)('Deploying to preview...').start();
131
131
  try {
132
132
  const projectDir = projectPath || process.cwd();
133
133
  const projectInfo = this.detectProjectInfo(projectDir);
@@ -160,7 +160,7 @@ class DeployCommand {
160
160
  * Deploy to Vigthoria subdomain
161
161
  */
162
162
  async deployToSubdomain(subdomain, projectPath) {
163
- const spinner = (0, ora_1.default)(`Deploying to ${subdomain}.vigthoria.io...`).start();
163
+ const spinner = (0, logger_js_1.createSpinner)(`Deploying to ${subdomain}.vigthoria.io...`).start();
164
164
  try {
165
165
  // Validate subdomain format
166
166
  if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(subdomain) || subdomain.length < 3) {
@@ -213,7 +213,7 @@ class DeployCommand {
213
213
  * Deploy to custom domain
214
214
  */
215
215
  async deployToCustomDomain(domain, projectPath) {
216
- const spinner = (0, ora_1.default)(`Setting up ${domain}...`).start();
216
+ const spinner = (0, logger_js_1.createSpinner)(`Setting up ${domain}...`).start();
217
217
  try {
218
218
  const projectDir = projectPath || process.cwd();
219
219
  const projectInfo = this.detectProjectInfo(projectDir);
@@ -303,7 +303,7 @@ class DeployCommand {
303
303
  * Show hosting plans
304
304
  */
305
305
  async showPlans() {
306
- const spinner = (0, ora_1.default)('Fetching hosting plans...').start();
306
+ const spinner = (0, logger_js_1.createSpinner)('Fetching hosting plans...').start();
307
307
  try {
308
308
  const response = await fetch(`${this.apiBase}/api/hosting/plans`, {
309
309
  headers: this.getAuthHeaders()
@@ -344,7 +344,7 @@ class DeployCommand {
344
344
  */
345
345
  async list() {
346
346
  this.requireAuth();
347
- const spinner = (0, ora_1.default)('Fetching deployments...').start();
347
+ const spinner = (0, logger_js_1.createSpinner)('Fetching deployments...').start();
348
348
  try {
349
349
  const response = await fetch(`${this.apiBase}/api/hosting/domains`, {
350
350
  headers: this.getAuthHeaders()
@@ -384,7 +384,7 @@ class DeployCommand {
384
384
  */
385
385
  async status(domain) {
386
386
  this.requireAuth();
387
- const spinner = (0, ora_1.default)('Checking status...').start();
387
+ const spinner = (0, logger_js_1.createSpinner)('Checking status...').start();
388
388
  try {
389
389
  const endpoint = domain
390
390
  ? `${this.apiBase}/api/hosting/domain/${encodeURIComponent(domain)}/status`
@@ -411,7 +411,7 @@ class DeployCommand {
411
411
  */
412
412
  async verify(domain) {
413
413
  this.requireAuth();
414
- const spinner = (0, ora_1.default)(`Verifying DNS for ${domain}...`).start();
414
+ const spinner = (0, logger_js_1.createSpinner)(`Verifying DNS for ${domain}...`).start();
415
415
  try {
416
416
  const response = await fetch(`${this.apiBase}/api/hosting/domain/verify`, {
417
417
  method: 'POST',
@@ -454,7 +454,7 @@ class DeployCommand {
454
454
  console.log(chalk_1.default.yellow('\n⚠️ Removal cancelled.\n'));
455
455
  return;
456
456
  }
457
- const spinner = (0, ora_1.default)(`Removing ${domain}...`).start();
457
+ const spinner = (0, logger_js_1.createSpinner)(`Removing ${domain}...`).start();
458
458
  try {
459
459
  const response = await fetch(`${this.apiBase}/api/hosting/domain/${encodeURIComponent(domain)}`, {
460
460
  method: 'DELETE',
@@ -8,8 +8,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.EditCommand = void 0;
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
- const ora_1 = __importDefault(require("ora"));
12
11
  const inquirer_1 = __importDefault(require("inquirer"));
12
+ const logger_js_1 = require("../utils/logger.js");
13
13
  const api_js_1 = require("../utils/api.js");
14
14
  const files_js_1 = require("../utils/files.js");
15
15
  class EditCommand {
@@ -52,7 +52,7 @@ class EditCommand {
52
52
  instruction = answer.instruction;
53
53
  }
54
54
  // Generate edit
55
- const spinner = (0, ora_1.default)({
55
+ const spinner = (0, logger_js_1.createSpinner)({
56
56
  text: 'Generating changes...',
57
57
  spinner: 'dots',
58
58
  }).start();
@@ -109,7 +109,7 @@ Return the complete modified code:`,
109
109
  this.logger.section(`Fixing: ${file.relativePath}`);
110
110
  console.log(chalk_1.default.gray(`Fix type: ${options.type} | Language: ${file.language}`));
111
111
  console.log();
112
- const spinner = (0, ora_1.default)({
112
+ const spinner = (0, logger_js_1.createSpinner)({
113
113
  text: `Analyzing for ${options.type} issues...`,
114
114
  spinner: 'dots',
115
115
  }).start();
@@ -8,9 +8,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.ExplainCommand = void 0;
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
- const ora_1 = __importDefault(require("ora"));
12
11
  const marked_1 = require("marked");
13
12
  const marked_terminal_1 = require("marked-terminal");
13
+ const logger_js_1 = require("../utils/logger.js");
14
14
  const api_js_1 = require("../utils/api.js");
15
15
  const files_js_1 = require("../utils/files.js");
16
16
  class ExplainCommand {
@@ -61,7 +61,7 @@ class ExplainCommand {
61
61
  });
62
62
  console.log(chalk_1.default.gray('─'.repeat(60)));
63
63
  console.log();
64
- const spinner = (0, ora_1.default)({
64
+ const spinner = (0, logger_js_1.createSpinner)({
65
65
  text: 'Analyzing code...',
66
66
  spinner: 'dots',
67
67
  }).start();
@@ -10,8 +10,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.GenerateCommand = void 0;
12
12
  const chalk_1 = __importDefault(require("chalk"));
13
- const ora_1 = __importDefault(require("ora"));
14
13
  const inquirer_1 = __importDefault(require("inquirer"));
14
+ const logger_js_1 = require("../utils/logger.js");
15
15
  const api_js_1 = require("../utils/api.js");
16
16
  const files_js_1 = require("../utils/files.js");
17
17
  class GenerateCommand {
@@ -40,7 +40,7 @@ class GenerateCommand {
40
40
  console.log(chalk_1.default.cyan('Pro Mode: Planning → Generating → Quality Check'));
41
41
  }
42
42
  console.log();
43
- const spinner = (0, ora_1.default)({
43
+ const spinner = (0, logger_js_1.createSpinner)({
44
44
  text: proMode ? 'Phase 1: Planning project structure...' : 'Generating code...',
45
45
  spinner: 'dots',
46
46
  }).start();
@@ -54,7 +54,7 @@ exports.RepoCommand = void 0;
54
54
  const chalk_1 = __importDefault(require("chalk"));
55
55
  const fs = __importStar(require("fs"));
56
56
  const path = __importStar(require("path"));
57
- const ora_1 = __importDefault(require("ora"));
57
+ const logger_js_1 = require("../utils/logger.js");
58
58
  const inquirer_1 = __importDefault(require("inquirer"));
59
59
  const archiver_1 = __importDefault(require("archiver"));
60
60
  const fs_1 = require("fs");
@@ -313,7 +313,7 @@ class RepoCommand {
313
313
  console.log(chalk_1.default.red(`\n❌ Path does not exist: ${projectPath}\n`));
314
314
  return;
315
315
  }
316
- const spinner = (0, ora_1.default)('Analyzing project...').start();
316
+ const spinner = (0, logger_js_1.createSpinner)('Analyzing project...').start();
317
317
  try {
318
318
  const projectInfo = this.detectProjectInfo(projectPath);
319
319
  spinner.succeed(`Project detected: ${chalk_1.default.cyan(projectInfo.name)}`);
@@ -363,7 +363,7 @@ class RepoCommand {
363
363
  console.log(chalk_1.default.yellow('\n⚠️ Push cancelled.\n'));
364
364
  return;
365
365
  }
366
- const uploadSpinner = (0, ora_1.default)('Preparing project files...').start();
366
+ const uploadSpinner = (0, logger_js_1.createSpinner)('Preparing project files...').start();
367
367
  const files = this.collectProjectFiles(projectPath);
368
368
  if (files.length === 0) {
369
369
  throw new Error('No readable text files found to push');
@@ -409,7 +409,7 @@ class RepoCommand {
409
409
  */
410
410
  async pull(projectName, options = {}) {
411
411
  this.requireAuth();
412
- const spinner = (0, ora_1.default)(`Fetching project: ${projectName}...`).start();
412
+ const spinner = (0, logger_js_1.createSpinner)(`Fetching project: ${projectName}...`).start();
413
413
  try {
414
414
  const repo = await this.resolveRepoByName(projectName);
415
415
  const response = await this.repoFetch('/api/repo/pull', {
@@ -437,7 +437,7 @@ class RepoCommand {
437
437
  return;
438
438
  }
439
439
  }
440
- const downloadSpinner = (0, ora_1.default)('Downloading project files...').start();
440
+ const downloadSpinner = (0, logger_js_1.createSpinner)('Downloading project files...').start();
441
441
  // Create output directory
442
442
  fs.mkdirSync(outputPath, { recursive: true });
443
443
  // If we have a download URL, fetch and extract
@@ -496,7 +496,7 @@ class RepoCommand {
496
496
  */
497
497
  async list(options = {}) {
498
498
  this.requireAuth();
499
- const spinner = (0, ora_1.default)('Fetching your projects...').start();
499
+ const spinner = (0, logger_js_1.createSpinner)('Fetching your projects...').start();
500
500
  try {
501
501
  const repos = await this.getMyRepos();
502
502
  const filteredRepos = options.visibility
@@ -596,7 +596,7 @@ class RepoCommand {
596
596
  this.requireAuth();
597
597
  const projectPath = process.cwd();
598
598
  const projectInfo = this.detectProjectInfo(projectPath);
599
- const spinner = (0, ora_1.default)('Checking sync status...').start();
599
+ const spinner = (0, logger_js_1.createSpinner)('Checking sync status...').start();
600
600
  try {
601
601
  const response = await fetch(`${this.apiBase}/api/repo/status/${encodeURIComponent(projectInfo.name)}`, {
602
602
  method: 'GET',
@@ -644,7 +644,7 @@ class RepoCommand {
644
644
  */
645
645
  async share(projectName, options = {}) {
646
646
  this.requireAuth();
647
- const spinner = (0, ora_1.default)('Generating share link...').start();
647
+ const spinner = (0, logger_js_1.createSpinner)('Generating share link...').start();
648
648
  try {
649
649
  const response = await fetch(`${this.apiBase}/api/repo/share`, {
650
650
  method: 'POST',
@@ -686,7 +686,7 @@ class RepoCommand {
686
686
  console.log(chalk_1.default.yellow('\n⚠️ Delete cancelled.\n'));
687
687
  return;
688
688
  }
689
- const spinner = (0, ora_1.default)('Deleting project...').start();
689
+ const spinner = (0, logger_js_1.createSpinner)('Deleting project...').start();
690
690
  try {
691
691
  const response = await fetch(`${this.apiBase}/api/repo/projects/${encodeURIComponent(projectName)}`, {
692
692
  method: 'DELETE',
@@ -712,7 +712,7 @@ class RepoCommand {
712
712
  // Parse project URL or name
713
713
  const projectIdMatch = projectUrl.match(/preview\/(\d+)/);
714
714
  const projectId = projectIdMatch ? projectIdMatch[1] : projectUrl;
715
- const spinner = (0, ora_1.default)(`Cloning project...`).start();
715
+ const spinner = (0, logger_js_1.createSpinner)(`Cloning project...`).start();
716
716
  try {
717
717
  const response = await fetch(`${this.apiBase}/api/repo/clone/${projectId}`, {
718
718
  method: 'GET',
@@ -8,9 +8,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.ReviewCommand = void 0;
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
- const ora_1 = __importDefault(require("ora"));
12
11
  const marked_1 = require("marked");
13
12
  const marked_terminal_1 = require("marked-terminal");
13
+ const logger_js_1 = require("../utils/logger.js");
14
14
  const api_js_1 = require("../utils/api.js");
15
15
  const files_js_1 = require("../utils/files.js");
16
16
  class ReviewCommand {
@@ -42,7 +42,7 @@ class ReviewCommand {
42
42
  this.logger.section(`Reviewing: ${file.relativePath}`);
43
43
  console.log(chalk_1.default.gray(`Language: ${file.language} | Lines: ${file.lines}`));
44
44
  console.log();
45
- const spinner = (0, ora_1.default)({
45
+ const spinner = (0, logger_js_1.createSpinner)({
46
46
  text: 'Analyzing code quality...',
47
47
  spinner: 'dots',
48
48
  }).start();
package/dist/index.js CHANGED
@@ -346,12 +346,17 @@ async function main() {
346
346
  });
347
347
  // Fix command - Fix code issues
348
348
  program
349
- .command('fix <file>')
349
+ .command('fix [file]')
350
350
  .alias('f')
351
351
  .description('Fix issues in a file')
352
352
  .option('-t, --type <type>', 'Fix type (bugs, style, security, performance)', 'bugs')
353
353
  .option('--apply', 'Automatically apply fixes', false)
354
354
  .action(async (file, options) => {
355
+ if (!file) {
356
+ logger.error('Usage: vigthoria fix <file> [--type bugs|style|security|performance] [--apply]');
357
+ process.exitCode = 1;
358
+ return;
359
+ }
355
360
  const edit = new edit_js_1.EditCommand(config, logger);
356
361
  await edit.fix(file, options);
357
362
  });
@@ -237,6 +237,11 @@ export declare class APIClient {
237
237
  private isLikelyWindowsPath;
238
238
  private resolveServerBindableWorkspacePath;
239
239
  private buildLocalWorkspaceSummary;
240
+ /**
241
+ * Collect text file contents from the workspace for V3 agent hydration.
242
+ * Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
243
+ */
244
+ collectWorkspaceFileContents(rootPath: string, filePaths: string[]): Record<string, string>;
240
245
  hasAgentWorkspaceOutput(context?: Record<string, any>): boolean;
241
246
  getAgentWorkspaceSnapshot(rootPath: string): {
242
247
  fileCount: number;
package/dist/utils/api.js CHANGED
@@ -593,26 +593,37 @@ class APIClient {
593
593
  error: artifacts.error || 'Frontend preview gate could not collect frontend artifacts.',
594
594
  };
595
595
  }
596
- const html = artifacts.html;
597
- const css = artifacts.css || '';
598
- const js = artifacts.js || '';
596
+ // Cap artifact sizes to prevent 413 Payload Too Large
597
+ const PREVIEW_MAX_ARTIFACT_BYTES = 500 * 1024;
598
+ const html = artifacts.html.slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
599
+ const css = (artifacts.css || '').slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
600
+ const js = (artifacts.js || '').slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
599
601
  const errors = [];
600
602
  for (const baseUrl of this.getTemplateServiceBaseUrls()) {
601
603
  try {
604
+ const proofPayload = JSON.stringify({
605
+ vision: String(context.rawPrompt || message || '').slice(0, 2000),
606
+ html,
607
+ css,
608
+ js,
609
+ entryPath: artifacts.htmlPath,
610
+ workspaceName: path_1.default.basename(rootPath),
611
+ });
612
+ // Skip preview proof if payload is still too large (> 4 MB)
613
+ if (Buffer.byteLength(proofPayload, 'utf8') > 4 * 1024 * 1024) {
614
+ return {
615
+ required: true,
616
+ passed: true,
617
+ error: 'Preview proof skipped: payload exceeds size limit.',
618
+ };
619
+ }
602
620
  const response = await fetch(`${baseUrl}/preview-proof`, {
603
621
  method: 'POST',
604
622
  headers: {
605
623
  'Content-Type': 'application/json',
606
624
  Accept: 'application/json',
607
625
  },
608
- body: JSON.stringify({
609
- vision: String(context.rawPrompt || message || ''),
610
- html,
611
- css,
612
- js,
613
- entryPath: artifacts.htmlPath,
614
- workspaceName: path_1.default.basename(rootPath),
615
- }),
626
+ body: proofPayload,
616
627
  });
617
628
  if (!response.ok) {
618
629
  const errorText = await response.text().catch(() => '');
@@ -1553,6 +1564,9 @@ menu {
1553
1564
  if (fs_1.default.existsSync(readmePath)) {
1554
1565
  summary.readmeExcerpt = fs_1.default.readFileSync(readmePath, 'utf8').slice(0, 2500);
1555
1566
  }
1567
+ // Hydrate workspace: include actual file contents so the V3 server
1568
+ // can populate the remote workspace before the agent starts.
1569
+ summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, snapshot.paths);
1556
1570
  return summary;
1557
1571
  }
1558
1572
  catch (error) {
@@ -1560,6 +1574,52 @@ menu {
1560
1574
  return null;
1561
1575
  }
1562
1576
  }
1577
+ /**
1578
+ * Collect text file contents from the workspace for V3 agent hydration.
1579
+ * Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
1580
+ */
1581
+ collectWorkspaceFileContents(rootPath, filePaths) {
1582
+ const MAX_TOTAL_BYTES = 2 * 1024 * 1024;
1583
+ const MAX_FILE_BYTES = 200 * 1024;
1584
+ const BINARY_EXTENSIONS = new Set([
1585
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp', '.avif',
1586
+ '.mp3', '.mp4', '.wav', '.ogg', '.webm', '.flac', '.aac',
1587
+ '.zip', '.gz', '.tar', '.rar', '.7z', '.bz2',
1588
+ '.exe', '.dll', '.so', '.dylib', '.bin', '.dat',
1589
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
1590
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
1591
+ '.db', '.sqlite', '.sqlite3',
1592
+ '.pyc', '.pyo', '.class', '.o', '.obj',
1593
+ '.DS_Store', '.lock',
1594
+ ]);
1595
+ const result = {};
1596
+ let totalBytes = 0;
1597
+ for (const relativePath of filePaths) {
1598
+ if (totalBytes >= MAX_TOTAL_BYTES)
1599
+ break;
1600
+ const ext = path_1.default.extname(relativePath).toLowerCase();
1601
+ if (BINARY_EXTENSIONS.has(ext))
1602
+ continue;
1603
+ if (/(^|[\/\\])\.(git|hg)([\/\\]|$)/.test(relativePath))
1604
+ continue;
1605
+ const absolutePath = path_1.default.join(rootPath, relativePath);
1606
+ try {
1607
+ const stat = fs_1.default.statSync(absolutePath);
1608
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES || stat.size === 0)
1609
+ continue;
1610
+ const content = fs_1.default.readFileSync(absolutePath, 'utf8');
1611
+ // Skip likely binary content (high ratio of non-printable chars)
1612
+ if (/[\x00-\x08\x0e-\x1f]/.test(content.slice(0, 512)))
1613
+ continue;
1614
+ result[relativePath] = content;
1615
+ totalBytes += Buffer.byteLength(content, 'utf8');
1616
+ }
1617
+ catch {
1618
+ // Skip unreadable files
1619
+ }
1620
+ }
1621
+ return result;
1622
+ }
1563
1623
  hasAgentWorkspaceOutput(context = {}) {
1564
1624
  try {
1565
1625
  const root = this.resolveAgentTargetPath(context);
@@ -2231,6 +2291,30 @@ document.addEventListener('DOMContentLoaded', () => {
2231
2291
  serverWorkspaceRoot = event.workspace_root.trim();
2232
2292
  }
2233
2293
  this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
2294
+ // Empty workspace guard: if the remote agent lists its root
2295
+ // and finds nothing while our local workspace has files, the
2296
+ // workspace was not hydrated. Abort early with a clear error
2297
+ // instead of letting the agent spin on an empty directory.
2298
+ if (event.type === 'tool_result'
2299
+ && event.name === 'list_directory'
2300
+ && event.success === true
2301
+ && serverWorkspaceRoot
2302
+ && typeof event.output === 'string') {
2303
+ const listOutput = event.output.trim();
2304
+ const looksEmpty = listOutput === `[${serverWorkspaceRoot}]`
2305
+ || listOutput === `[${serverWorkspaceRoot}/]`
2306
+ || listOutput === `[${serverWorkspaceRoot}]\\n`
2307
+ || /^\[\/tmp\/vig-remote-server-[^\]]+\]\s*$/.test(listOutput);
2308
+ if (looksEmpty) {
2309
+ const localPath = this.resolveAgentTargetPath(context);
2310
+ const localHasFiles = localPath && fs_1.default.existsSync(localPath) && this.hasAgentWorkspaceOutput({ ...context, projectPath: localPath, targetPath: localPath });
2311
+ if (localHasFiles) {
2312
+ throw new Error('Remote workspace is empty — the V3 server did not receive your project files. '
2313
+ + 'Your local workspace has files but the remote agent sees an empty directory. '
2314
+ + 'This is a workspace sync failure. Falling back to local agent loop.');
2315
+ }
2316
+ }
2317
+ }
2234
2318
  if (typeof context.onStreamEvent === 'function') {
2235
2319
  try {
2236
2320
  context.onStreamEvent(event);
@@ -3096,32 +3180,87 @@ document.addEventListener('DOMContentLoaded', () => {
3096
3180
  }
3097
3181
  }
3098
3182
  async getV3AgentHealth() {
3099
- const endpoint = this.getV3AgentRunUrl(this.getV3AgentBaseUrls()[0]).replace('/api/agent/run', '/health');
3100
- try {
3101
- const response = await fetch(endpoint, {
3102
- method: 'GET',
3103
- headers: await this.getV3AgentHeaders(),
3104
- });
3105
- if (!response.ok) {
3183
+ const baseUrl = this.getV3AgentBaseUrls()[0];
3184
+ // Try multiple health endpoint patterns — the V3 backend may expose
3185
+ // different paths depending on whether it's local (8030) or remote.
3186
+ const candidates = [
3187
+ `${baseUrl}/api/v3-agent/health`,
3188
+ `${baseUrl}/api/health`,
3189
+ `${baseUrl}/health`,
3190
+ ];
3191
+ const headers = await this.getV3AgentHeaders();
3192
+ for (const endpoint of candidates) {
3193
+ try {
3194
+ const controller = new AbortController();
3195
+ const timer = setTimeout(() => controller.abort(), 8000);
3196
+ const response = await fetch(endpoint, {
3197
+ method: 'GET',
3198
+ headers,
3199
+ signal: controller.signal,
3200
+ });
3201
+ clearTimeout(timer);
3202
+ if (response.ok) {
3203
+ const data = await response.json().catch(() => ({}));
3204
+ return {
3205
+ name: 'V3 Agent',
3206
+ endpoint,
3207
+ ok: true,
3208
+ details: { health: data },
3209
+ };
3210
+ }
3211
+ // 404 means this path doesn't exist — try next candidate
3212
+ if (response.status === 404)
3213
+ continue;
3214
+ // 405 Method Not Allowed — endpoint exists but rejects GET.
3215
+ // Treat as reachable since the run endpoint (POST) will work.
3216
+ if (response.status === 405) {
3217
+ return {
3218
+ name: 'V3 Agent',
3219
+ endpoint,
3220
+ ok: true,
3221
+ details: { health: { reachable: true, note: 'Health endpoint returned 405 but run endpoint is available' } },
3222
+ };
3223
+ }
3224
+ // Other error
3106
3225
  throw new Error(`V3 health ${response.status}`);
3107
3226
  }
3108
- const data = await response.json();
3109
- const ok = data?.status === 'ok' || data?.healthy === true;
3110
- return {
3111
- name: 'V3 Agent',
3112
- endpoint,
3113
- ok,
3114
- details: { health: data },
3115
- };
3227
+ catch (error) {
3228
+ if (error instanceof Error && error.message.startsWith('V3 health')) {
3229
+ return {
3230
+ name: 'V3 Agent',
3231
+ endpoint,
3232
+ ok: false,
3233
+ error: error.message,
3234
+ };
3235
+ }
3236
+ // Network/timeout error — try next candidate
3237
+ }
3116
3238
  }
3117
- catch (error) {
3118
- return {
3119
- name: 'V3 Agent',
3120
- endpoint,
3121
- ok: false,
3122
- error: error instanceof Error ? error.message : String(error),
3123
- };
3239
+ // Last resort: probe the run endpoint with OPTIONS
3240
+ const runUrl = this.getV3AgentRunUrl(baseUrl);
3241
+ try {
3242
+ const controller = new AbortController();
3243
+ const timer = setTimeout(() => controller.abort(), 5000);
3244
+ const probe = await fetch(runUrl, { method: 'OPTIONS', headers, signal: controller.signal });
3245
+ clearTimeout(timer);
3246
+ if (probe.ok || probe.status === 204 || probe.status === 405) {
3247
+ return {
3248
+ name: 'V3 Agent',
3249
+ endpoint: runUrl,
3250
+ ok: true,
3251
+ details: { health: { reachable: true, method: 'OPTIONS' } },
3252
+ };
3253
+ }
3124
3254
  }
3255
+ catch {
3256
+ // final attempt failed
3257
+ }
3258
+ return {
3259
+ name: 'V3 Agent',
3260
+ endpoint: candidates[0],
3261
+ ok: false,
3262
+ error: 'No V3 agent health endpoint responded.',
3263
+ };
3125
3264
  }
3126
3265
  async getHyperLoopHealth() {
3127
3266
  const endpoint = process.env.VIGTHORIA_HYPERLOOP_URL || 'http://127.0.0.1:8020/api/hyperloop/health';
@@ -1,6 +1,13 @@
1
1
  /**
2
2
  * Logger utility for Vigthoria CLI
3
3
  */
4
+ import { type Options as OraOptions, type Ora } from 'ora';
5
+ export type { Ora };
6
+ /**
7
+ * Create an ora spinner that writes to stderr so it never
8
+ * pollutes stdout JSON output or triggers PowerShell error styling.
9
+ */
10
+ export declare function createSpinner(textOrOpts: string | OraOptions): Ora;
4
11
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success';
5
12
  export declare class Logger {
6
13
  private verbose;
@@ -7,7 +7,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.Logger = void 0;
10
+ exports.createSpinner = createSpinner;
10
11
  const chalk_1 = __importDefault(require("chalk"));
12
+ const ora_1 = __importDefault(require("ora"));
13
+ /**
14
+ * Create an ora spinner that writes to stderr so it never
15
+ * pollutes stdout JSON output or triggers PowerShell error styling.
16
+ */
17
+ function createSpinner(textOrOpts) {
18
+ const opts = typeof textOrOpts === 'string' ? { text: textOrOpts } : textOrOpts;
19
+ return (0, ora_1.default)({ ...opts, stream: process.stderr });
20
+ }
11
21
  class Logger {
12
22
  verbose = false;
13
23
  setVerbose(verbose) {
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { Logger } from './logger.js';
17
17
  export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
18
+ export type SearchStatus = 'search_matches_found' | 'search_no_matches' | 'search_failed';
18
19
  export interface ToolResult {
19
20
  success: boolean;
20
21
  output?: string;
@@ -23,6 +24,7 @@ export interface ToolResult {
23
24
  canRetry?: boolean;
24
25
  undoable?: boolean;
25
26
  metadata?: {
27
+ searchStatus?: SearchStatus;
26
28
  [key: string]: any;
27
29
  };
28
30
  }
@@ -145,6 +147,26 @@ export declare class AgenticTools {
145
147
  */
146
148
  private bash;
147
149
  private grep;
150
+ /**
151
+ * Windows grep: rg > Select-String > Node-native
152
+ */
153
+ private grepWindows;
154
+ /**
155
+ * Unix grep: rg > system grep (BSD/GNU)
156
+ */
157
+ private grepUnix;
158
+ /**
159
+ * Search using ripgrep (rg) — works on all platforms
160
+ */
161
+ private grepWithRg;
162
+ /**
163
+ * Search using PowerShell Select-String (Windows)
164
+ */
165
+ private grepWithSelectString;
166
+ /**
167
+ * Pure Node.js recursive file search — reliable on all platforms, no external deps
168
+ */
169
+ private grepNodeNative;
148
170
  private listDir;
149
171
  private glob;
150
172
  private git;
@@ -995,48 +995,270 @@ class AgenticTools {
995
995
  }
996
996
  grep(args) {
997
997
  const searchPath = args.path ? this.resolvePath(args.path) : this.cwd;
998
- const include = args.include || '*';
999
- const os = require('os');
1000
- // macOS uses BSD grep which has different options than GNU grep
1001
- const isMac = os.platform() === 'darwin';
998
+ const osModule = require('os');
999
+ const platform = osModule.platform();
1000
+ // If forced fallback, go directly to Node-native
1001
+ if (args._fallback === 'node-native') {
1002
+ return this.grepNodeNative(args, searchPath);
1003
+ }
1004
+ // Try external search tools first (rg > grep/Select-String), fall back to Node-native
1005
+ if (platform === 'win32') {
1006
+ return this.grepWindows(args, searchPath);
1007
+ }
1008
+ return this.grepUnix(args, searchPath, platform);
1009
+ }
1010
+ /**
1011
+ * Windows grep: rg > Select-String > Node-native
1012
+ */
1013
+ grepWindows(args, searchPath) {
1014
+ // 1. Try ripgrep (rg) first — best cross-platform search tool
1015
+ try {
1016
+ (0, child_process_1.execSync)('rg --version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
1017
+ return this.grepWithRg(args, searchPath);
1018
+ }
1019
+ catch { /* rg not available */ }
1020
+ // 2. Try PowerShell Select-String
1021
+ try {
1022
+ return this.grepWithSelectString(args, searchPath);
1023
+ }
1024
+ catch { /* Select-String failed */ }
1025
+ // 3. Fall back to Node-native recursive file scanner
1026
+ return this.grepNodeNative(args, searchPath);
1027
+ }
1028
+ /**
1029
+ * Unix grep: rg > system grep (BSD/GNU)
1030
+ */
1031
+ grepUnix(args, searchPath, platform) {
1032
+ // Try ripgrep first if available
1033
+ try {
1034
+ (0, child_process_1.execSync)('rg --version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
1035
+ return this.grepWithRg(args, searchPath);
1036
+ }
1037
+ catch { /* rg not available, fall through to system grep */ }
1038
+ const isMac = platform === 'darwin';
1002
1039
  let cmd;
1003
1040
  if (isMac) {
1004
- // BSD grep: use -r for recursive, -n for line numbers
1005
- // Note: BSD grep doesn't support --color=never, just omit it
1006
- if (args.include) {
1007
- cmd = `grep -rn --include="${args.include}" "${args.pattern}" "${searchPath}"`;
1008
- }
1009
- else {
1010
- cmd = `grep -rn "${args.pattern}" "${searchPath}"`;
1011
- }
1041
+ cmd = args.include
1042
+ ? `grep -rn --include="${args.include}" "${args.pattern}" "${searchPath}"`
1043
+ : `grep -rn "${args.pattern}" "${searchPath}"`;
1012
1044
  }
1013
1045
  else {
1014
- // GNU grep (Linux)
1015
- if (args.include) {
1016
- cmd = `grep -rn --color=never --include="${args.include}" "${args.pattern}" "${searchPath}"`;
1017
- }
1018
- else {
1019
- cmd = `grep -rn --color=never "${args.pattern}" "${searchPath}"`;
1046
+ cmd = args.include
1047
+ ? `grep -rn --color=never --include="${args.include}" "${args.pattern}" "${searchPath}"`
1048
+ : `grep -rn --color=never "${args.pattern}" "${searchPath}"`;
1049
+ }
1050
+ try {
1051
+ const output = (0, child_process_1.execSync)(cmd, {
1052
+ cwd: this.cwd,
1053
+ encoding: 'utf-8',
1054
+ maxBuffer: 5 * 1024 * 1024,
1055
+ timeout: 30000,
1056
+ env: { ...process.env, GREP_OPTIONS: '' },
1057
+ });
1058
+ return {
1059
+ success: true,
1060
+ output: output.trim(),
1061
+ metadata: { searchStatus: 'search_matches_found' },
1062
+ };
1063
+ }
1064
+ catch (error) {
1065
+ // Distinguish real "no matches" (exit 1 + no stderr) from command failure
1066
+ const stderr = (error.stderr || '').toString();
1067
+ if (error.status === 1 && !stderr.trim()) {
1068
+ return {
1069
+ success: true,
1070
+ output: 'No matches found',
1071
+ metadata: { searchStatus: 'search_no_matches' },
1072
+ };
1020
1073
  }
1074
+ // Real failure: command not found, permission denied, etc.
1075
+ return {
1076
+ success: false,
1077
+ error: stderr || error.message,
1078
+ suggestion: 'The grep command failed. Install ripgrep (rg) for best cross-platform search.',
1079
+ canRetry: true,
1080
+ metadata: { searchStatus: 'search_failed' },
1081
+ };
1082
+ }
1083
+ }
1084
+ /**
1085
+ * Search using ripgrep (rg) — works on all platforms
1086
+ */
1087
+ grepWithRg(args, searchPath) {
1088
+ let cmd = `rg -n --no-heading`;
1089
+ if (args.include) {
1090
+ cmd += ` -g "${args.include}"`;
1021
1091
  }
1092
+ cmd += ` "${args.pattern}" "${searchPath}"`;
1022
1093
  try {
1023
1094
  const output = (0, child_process_1.execSync)(cmd, {
1024
1095
  cwd: this.cwd,
1025
1096
  encoding: 'utf-8',
1026
1097
  maxBuffer: 5 * 1024 * 1024,
1027
1098
  timeout: 30000,
1028
- env: { ...process.env, GREP_OPTIONS: '' }, // Prevent user's grep options from interfering
1029
1099
  });
1030
- return { success: true, output: output.trim() };
1100
+ return {
1101
+ success: true,
1102
+ output: output.trim(),
1103
+ metadata: { searchStatus: 'search_matches_found', backend: 'ripgrep' },
1104
+ };
1031
1105
  }
1032
1106
  catch (error) {
1033
- if (error.status === 1) {
1034
- // No matches found
1035
- return { success: true, output: 'No matches found' };
1107
+ if (error.status === 1 && !(error.stderr || '').toString().trim()) {
1108
+ return {
1109
+ success: true,
1110
+ output: 'No matches found',
1111
+ metadata: { searchStatus: 'search_no_matches', backend: 'ripgrep' },
1112
+ };
1036
1113
  }
1037
- return { success: false, error: error.message };
1114
+ throw error; // Let caller try next backend
1038
1115
  }
1039
1116
  }
1117
+ /**
1118
+ * Search using PowerShell Select-String (Windows)
1119
+ */
1120
+ grepWithSelectString(args, searchPath) {
1121
+ // Normalize path for PowerShell
1122
+ const psPath = searchPath.replace(/\//g, '\\');
1123
+ const includeFilter = args.include
1124
+ ? `-Include "${args.include}"`
1125
+ : `-Include *`;
1126
+ const escapedPattern = args.pattern.replace(/'/g, "''");
1127
+ const cmd = `powershell -NoProfile -Command "Get-ChildItem -Path '${psPath}' -Recurse -File ${includeFilter} | Select-String -Pattern '${escapedPattern}' | ForEach-Object { $_.Path + ':' + $_.LineNumber + ':' + $_.Line }"`;
1128
+ try {
1129
+ const output = (0, child_process_1.execSync)(cmd, {
1130
+ cwd: this.cwd,
1131
+ encoding: 'utf-8',
1132
+ maxBuffer: 5 * 1024 * 1024,
1133
+ timeout: 60000,
1134
+ });
1135
+ const trimmed = output.trim();
1136
+ if (!trimmed) {
1137
+ return {
1138
+ success: true,
1139
+ output: 'No matches found',
1140
+ metadata: { searchStatus: 'search_no_matches', backend: 'select-string' },
1141
+ };
1142
+ }
1143
+ return {
1144
+ success: true,
1145
+ output: trimmed,
1146
+ metadata: { searchStatus: 'search_matches_found', backend: 'select-string' },
1147
+ };
1148
+ }
1149
+ catch (error) {
1150
+ const stderr = (error.stderr || '').toString();
1151
+ // Select-String returns exit code 1 for no matches when used in pipeline
1152
+ if (error.status === 1 && !stderr.trim()) {
1153
+ return {
1154
+ success: true,
1155
+ output: 'No matches found',
1156
+ metadata: { searchStatus: 'search_no_matches', backend: 'select-string' },
1157
+ };
1158
+ }
1159
+ throw error; // Let caller try next backend
1160
+ }
1161
+ }
1162
+ /**
1163
+ * Pure Node.js recursive file search — reliable on all platforms, no external deps
1164
+ */
1165
+ grepNodeNative(args, searchPath) {
1166
+ const { minimatch } = (() => {
1167
+ try {
1168
+ return require('minimatch');
1169
+ }
1170
+ catch {
1171
+ return { minimatch: null };
1172
+ }
1173
+ })();
1174
+ const results = [];
1175
+ let regex;
1176
+ try {
1177
+ regex = new RegExp(args.pattern, 'i');
1178
+ }
1179
+ catch {
1180
+ // If pattern is not valid regex, treat as literal
1181
+ regex = new RegExp(args.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
1182
+ }
1183
+ const includeGlob = args.include || null;
1184
+ const maxResults = 500;
1185
+ const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '__pycache__', '.next', 'vendor', '.venv']);
1186
+ const walk = (dir) => {
1187
+ if (results.length >= maxResults)
1188
+ return;
1189
+ let entries;
1190
+ try {
1191
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1192
+ }
1193
+ catch {
1194
+ return; // Permission denied or inaccessible
1195
+ }
1196
+ for (const entry of entries) {
1197
+ if (results.length >= maxResults)
1198
+ break;
1199
+ const fullPath = path.join(dir, entry.name);
1200
+ if (entry.isDirectory()) {
1201
+ if (!skipDirs.has(entry.name)) {
1202
+ walk(fullPath);
1203
+ }
1204
+ continue;
1205
+ }
1206
+ if (!entry.isFile())
1207
+ continue;
1208
+ // Check include pattern
1209
+ if (includeGlob) {
1210
+ const matches = minimatch
1211
+ ? minimatch(entry.name, includeGlob)
1212
+ : entry.name.endsWith(includeGlob.replace('*', ''));
1213
+ if (!matches)
1214
+ continue;
1215
+ }
1216
+ // Skip binary files by extension
1217
+ const ext = path.extname(entry.name).toLowerCase();
1218
+ const binaryExts = new Set(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.zip', '.tar', '.gz', '.exe', '.dll', '.so', '.dylib', '.pdf', '.mp3', '.mp4', '.wav', '.avi', '.mov']);
1219
+ if (binaryExts.has(ext))
1220
+ continue;
1221
+ try {
1222
+ const content = fs.readFileSync(fullPath, 'utf-8');
1223
+ const lines = content.split('\n');
1224
+ for (let i = 0; i < lines.length; i++) {
1225
+ if (regex.test(lines[i])) {
1226
+ const relativePath = path.relative(this.cwd, fullPath).replace(/\\/g, '/');
1227
+ results.push(`${relativePath}:${i + 1}:${lines[i]}`);
1228
+ if (results.length >= maxResults)
1229
+ break;
1230
+ }
1231
+ }
1232
+ }
1233
+ catch {
1234
+ // Skip files that can't be read (binary, encoding issues, etc.)
1235
+ }
1236
+ }
1237
+ };
1238
+ try {
1239
+ walk(searchPath);
1240
+ }
1241
+ catch (error) {
1242
+ return {
1243
+ success: false,
1244
+ error: `File search failed: ${error.message}`,
1245
+ metadata: { searchStatus: 'search_failed', backend: 'node-native' },
1246
+ };
1247
+ }
1248
+ if (results.length === 0) {
1249
+ return {
1250
+ success: true,
1251
+ output: 'No matches found',
1252
+ metadata: { searchStatus: 'search_no_matches', backend: 'node-native' },
1253
+ };
1254
+ }
1255
+ const truncationNote = results.length >= maxResults ? `\n...[truncated at ${maxResults} matches]` : '';
1256
+ return {
1257
+ success: true,
1258
+ output: results.join('\n') + truncationNote,
1259
+ metadata: { searchStatus: 'search_matches_found', backend: 'node-native' },
1260
+ };
1261
+ }
1040
1262
  listDir(args) {
1041
1263
  const dirPath = this.resolvePath(args.path);
1042
1264
  if (!fs.existsSync(dirPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.16",
3
+ "version": "1.6.18",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [