@voria/cli 0.0.3 → 0.0.5

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.
Files changed (47) hide show
  1. package/README.md +75 -380
  2. package/bin/voria +635 -481
  3. package/docs/CHANGELOG.md +19 -0
  4. package/docs/USER_GUIDE.md +34 -5
  5. package/package.json +1 -1
  6. package/python/voria/__init__.py +1 -1
  7. package/python/voria/__pycache__/__init__.cpython-312.pyc +0 -0
  8. package/python/voria/__pycache__/engine.cpython-312.pyc +0 -0
  9. package/python/voria/core/__pycache__/__init__.cpython-312.pyc +0 -0
  10. package/python/voria/core/__pycache__/setup.cpython-312.pyc +0 -0
  11. package/python/voria/core/agent/__pycache__/__init__.cpython-312.pyc +0 -0
  12. package/python/voria/core/agent/__pycache__/loop.cpython-312.pyc +0 -0
  13. package/python/voria/core/executor/__pycache__/__init__.cpython-312.pyc +0 -0
  14. package/python/voria/core/executor/__pycache__/executor.cpython-312.pyc +0 -0
  15. package/python/voria/core/executor/executor.py +5 -0
  16. package/python/voria/core/github/__pycache__/__init__.cpython-312.pyc +0 -0
  17. package/python/voria/core/github/__pycache__/client.cpython-312.pyc +0 -0
  18. package/python/voria/core/llm/__init__.py +16 -0
  19. package/python/voria/core/llm/__pycache__/__init__.cpython-312.pyc +0 -0
  20. package/python/voria/core/llm/__pycache__/base.cpython-312.pyc +0 -0
  21. package/python/voria/core/llm/__pycache__/claude_provider.cpython-312.pyc +0 -0
  22. package/python/voria/core/llm/__pycache__/deepseek_provider.cpython-312.pyc +0 -0
  23. package/python/voria/core/llm/__pycache__/gemini_provider.cpython-312.pyc +0 -0
  24. package/python/voria/core/llm/__pycache__/kimi_provider.cpython-312.pyc +0 -0
  25. package/python/voria/core/llm/__pycache__/minimax_provider.cpython-312.pyc +0 -0
  26. package/python/voria/core/llm/__pycache__/modal_provider.cpython-312.pyc +0 -0
  27. package/python/voria/core/llm/__pycache__/model_discovery.cpython-312.pyc +0 -0
  28. package/python/voria/core/llm/__pycache__/openai_provider.cpython-312.pyc +0 -0
  29. package/python/voria/core/llm/__pycache__/siliconflow_provider.cpython-312.pyc +0 -0
  30. package/python/voria/core/llm/base.py +12 -0
  31. package/python/voria/core/llm/claude_provider.py +46 -0
  32. package/python/voria/core/llm/deepseek_provider.py +109 -0
  33. package/python/voria/core/llm/gemini_provider.py +44 -0
  34. package/python/voria/core/llm/kimi_provider.py +109 -0
  35. package/python/voria/core/llm/minimax_provider.py +187 -0
  36. package/python/voria/core/llm/modal_provider.py +33 -0
  37. package/python/voria/core/llm/model_discovery.py +104 -155
  38. package/python/voria/core/llm/openai_provider.py +33 -0
  39. package/python/voria/core/llm/siliconflow_provider.py +109 -0
  40. package/python/voria/core/patcher/__pycache__/__init__.cpython-312.pyc +0 -0
  41. package/python/voria/core/patcher/__pycache__/patcher.cpython-312.pyc +0 -0
  42. package/python/voria/core/setup.py +4 -1
  43. package/python/voria/core/testing/__pycache__/definitions.cpython-312.pyc +0 -0
  44. package/python/voria/core/testing/__pycache__/runner.cpython-312.pyc +0 -0
  45. package/python/voria/core/testing/definitions.py +87 -0
  46. package/python/voria/core/testing/runner.py +324 -0
  47. package/python/voria/engine.py +736 -232
package/bin/voria CHANGED
@@ -53,102 +53,182 @@ const BANNER = `
53
53
 
54
54
  const HELP_TEXT = `
55
55
  ${BANNER}
56
- ${colors.dim("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")}
57
- ${colors.dim("┃")} ${colors.bold("AI-Powered Autonomous Fixer")} ${colors.dim("┃")}
58
- ${colors.dim("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")}
56
+ ${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
57
+ ${colors.bold(colors.blue(" AI-Powered Autonomous Fixer v0.0.5"))}
58
+ ${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
59
59
 
60
60
  ${colors.bold(colors.blue("USAGE:"))}
61
61
  voria [command] [options]
62
62
 
63
- ${colors.bold(colors.blue("GLOBAL COMMANDS:"))}
64
- ${colors.blue("🚀")} voria --init ${colors.dim("Initialize project & choose models")}
65
- ${colors.blue("⚙️")} voria --config ${colors.dim("Configure LLM provider and settings")}
63
+ ${colors.bold(colors.blue("SETUP:"))}
64
+ ${colors.blue("🚀")} voria --init ${colors.dim("Initialize project & choose LLM provider")}
65
+ ${colors.blue("⚙️")} voria --config ${colors.dim("Configure LLM provider and settings")}
66
66
  ${colors.blue("🔑")} voria --set-github-token ${colors.dim("Set your GitHub personal access token")}
67
- ${colors.blue("📋")} voria --list-issues <repo>${colors.dim("List issues in a repository")}
68
- ${colors.blue("🔭")} voria --graph ${colors.dim("Show codebase dependency graph")}
69
- ${colors.blue("❓")} voria --help ${colors.dim("Show this help message")}
70
- ${colors.blue("🏷️")} voria --version ${colors.dim("Show current version")}
71
67
 
72
- ${colors.bold(colors.blue("PROJECT COMMANDS:"))}
73
- ${colors.blue("🐞")} voria issue <repo> <num> ${colors.dim("Fix a specific GitHub issue")}
68
+ ${colors.bold(colors.blue("SECURITY & TESTING:"))}
69
+ ${colors.blue("🛡️")} voria scan ${colors.dim("Full security audit run ALL tests in parallel")}
70
+ ${colors.blue("🧪")} voria test <test_id> ${colors.dim("Run a specific security/performance test")}
71
+ ${colors.blue("📋")} voria list-tests ${colors.dim("List all 25+ available tests")}
72
+ ${colors.blue("👁️")} voria watch ${colors.dim("File watcher — re-runs tests on code changes")}
73
+ ${colors.blue("📊")} voria diff [a] [b] ${colors.dim("Compare security posture between git refs")}
74
+ ${colors.blue("⚡")} voria benchmark <url> ${colors.dim("HTTP benchmarking — p50/p95/p99 latencies")}
75
+ ${colors.blue("🏗️")} voria ci ${colors.dim("CI/CD scan — outputs SARIF for GitHub Security")}
76
+
77
+ ${colors.bold(colors.blue("ISSUE FIXING:"))}
78
+ ${colors.blue("🐞")} voria issue <repo> <num> ${colors.dim("Fix a specific GitHub issue with AI")}
79
+ ${colors.blue("🔧")} voria fix --auto <r> <n> ${colors.dim("Auto-apply patch and run tests")}
80
+ ${colors.blue("📝")} voria plan <description> ${colors.dim("Generate fix plan without applying")}
74
81
  ${colors.blue("📦")} voria apply <patch> ${colors.dim("Apply a patch file")}
82
+ ${colors.blue("📋")} voria --list-issues <r> ${colors.dim("List issues in a repository")}
83
+
84
+ ${colors.bold(colors.blue("PROJECT:"))}
75
85
  ${colors.blue("📊")} voria status ${colors.dim("Show current project status")}
76
86
  ${colors.blue("💰")} voria token info ${colors.dim("Show token usage info")}
77
87
  ${colors.blue("📝")} voria logs ${colors.dim("Show recent activity logs")}
78
88
  ${colors.blue("🔄")} voria reset ${colors.dim("Reset project configuration")}
89
+ ${colors.blue("🔭")} voria --graph ${colors.dim("Show codebase dependency graph")}
79
90
 
80
91
  ${colors.bold(colors.blue("OPTIONS:"))}
81
- --llm <provider> ${colors.dim("Use specific LLM (openai, claude, gemini, modal, kimi)")}
82
- --model <name> ${colors.dim("Use specific model")}
92
+ --category <cat> ${colors.dim("Filter scan by category (security/performance/stress/quality/all)")}
93
+ --requests <n> ${colors.dim("Number of requests for benchmark (default: 100)")}
94
+ --concurrency <n> ${colors.dim("Concurrency for benchmark (default: 10)")}
83
95
  --apply ${colors.dim("Automatically create PR after fixing")}
84
96
  --dry-run ${colors.dim("Don't create PR, just propose")}
85
97
  --verbose, -v ${colors.dim("Show detailed output")}
86
98
 
87
99
  ${colors.bold(colors.blue("EXAMPLES:"))}
88
- ${colors.cyan("# Initialize a project")}
89
- voria --init
100
+ ${colors.cyan("voria --init")} ${colors.dim("# Setup with your LLM provider")}
101
+ ${colors.cyan("voria scan")} ${colors.dim("# Full security audit")}
102
+ ${colors.cyan("voria scan --category performance")} ${colors.dim("# Performance tests only")}
103
+ ${colors.cyan("voria test hardcoded_secrets")} ${colors.dim("# Run specific test")}
104
+ ${colors.cyan("voria diff main feature-branch")} ${colors.dim("# Compare branches")}
105
+ ${colors.cyan("voria benchmark http://localhost:3000")} ${colors.dim("# HTTP benchmark")}
106
+ ${colors.cyan("voria issue owner/repo 42 --apply")} ${colors.dim("# Fix issue and create PR")}
107
+ ${colors.cyan("voria ci")} ${colors.dim("# CI/CD SARIF output")}
108
+
109
+ ${colors.bold(colors.blue("DOCS:"))} ${colors.blue("https://github.com/Srizdebnath/voria")}
110
+ `;
90
111
 
91
- ${colors.cyan("# Fix an issue and create PR")}
92
- voria issue owner/repo 42 --apply
112
+ const VERSION = '0.0.5';
93
113
 
94
- ${colors.cyan("# Propose a fix with specific model")}
95
- voria plan "Remove deprecated function" --llm claude --model opus-4.6
114
+ // ─── Helpers ─────────────────────────────────────────────────────
96
115
 
97
- ${colors.bold(colors.blue("DOCUMENTATION:"))}
98
- ${colors.dim("Full documentation:")} ${colors.blue("https://github.com/Srizdebnath/voria")}
99
- `;
100
-
101
- const VERSION = '0.0.3';
116
+ function log(msg) { console.log(`${colors.blue("")} ${msg}`); }
117
+ function logHeader(msg) { console.log(`\n${colors.bgBlue(colors.bold(` ${msg} `))}`); }
118
+ function logDivider() { console.log(colors.blue("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); }
119
+ function logSuccess(msg) { console.log(`${colors.green("✓")} ${msg}`); }
120
+ function logError(msg) { console.error(`${colors.red("✖")} ${msg}`); }
121
+ function logInfo(msg) { console.log(`${colors.blue("ℹ")} ${colors.dim(msg)}`); }
122
+ function logWarn(msg) { console.log(`${colors.yellow("⚠")} ${msg}`); }
102
123
 
103
124
  // Load configuration
104
125
  function loadConfig() {
105
126
  const config = {};
106
-
107
127
  if (fs.existsSync(CONFIG_FILE)) {
108
128
  try {
109
- const data = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
110
- Object.assign(config, data);
129
+ Object.assign(config, JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')));
111
130
  } catch (e) {
112
- console.error('⚠️ Error reading global config:', e.message);
131
+ logWarn(`Error reading global config: ${e.message}`);
113
132
  }
114
133
  }
115
-
116
134
  if (fs.existsSync(PROJECT_CONFIG)) {
117
135
  try {
118
- const data = JSON.parse(fs.readFileSync(PROJECT_CONFIG, 'utf-8'));
119
- Object.assign(config, data);
136
+ Object.assign(config, JSON.parse(fs.readFileSync(PROJECT_CONFIG, 'utf-8')));
120
137
  } catch (e) {
121
- console.error('⚠️ Error reading project config:', e.message);
138
+ logWarn(`Error reading project config: ${e.message}`);
122
139
  }
123
140
  }
124
-
125
141
  return config;
126
142
  }
127
143
 
128
- // Save configuration
129
144
  function saveConfig(config, isGlobal = true) {
130
145
  const file = isGlobal ? CONFIG_FILE : PROJECT_CONFIG;
131
146
  fs.writeFileSync(file, JSON.stringify(config, null, 2));
132
147
  }
133
148
 
134
- // Interactive setup
135
- // Interactive setup
149
+ function requireInit() {
150
+ const config = loadConfig();
151
+ if (!config.llm_api_key && !config.llm_provider) {
152
+ logError(`voria not initialized. Run ${colors.cyan("voria --init")}`);
153
+ process.exit(1);
154
+ }
155
+ return config;
156
+ }
157
+
158
+ function getApiKey(config) {
159
+ let apiKey = config.llm_api_key;
160
+ if (!apiKey) {
161
+ const envKey = `${(config.llm_provider || 'openai').toUpperCase()}_API_KEY`;
162
+ apiKey = process.env[envKey];
163
+ }
164
+ if (!apiKey) {
165
+ logError(`API key not found. Run ${colors.cyan("voria --config")} or set env var.`);
166
+ process.exit(1);
167
+ }
168
+ return apiKey;
169
+ }
170
+
171
+ function fetchModels(provider, apiKey) {
172
+ try {
173
+ const scriptPath = path.join(__dirname, '..', 'python', 'voria', 'core', 'llm', 'model_discovery.py');
174
+ const output = execSync(`python3 "${scriptPath}" "${provider}" "${apiKey}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
175
+ return JSON.parse(output);
176
+ } catch (e) { return []; }
177
+ }
178
+
179
+ // ─── Python Engine IPC ───────────────────────────────────────────
180
+
181
+ async function callPythonEngine(command) {
182
+ return new Promise((resolve, reject) => {
183
+ const pythonScript = path.join(__dirname, '..', 'python', 'voria', 'engine.py');
184
+ const pythonPath = path.join(__dirname, '..', 'python');
185
+ const venvPath = path.join(__dirname, '..', 'venv', 'bin', 'python');
186
+ const pythonCmd = fs.existsSync(venvPath) ? venvPath : 'python3';
187
+ const env = { ...process.env };
188
+ env.PYTHONPATH = pythonPath + (env.PYTHONPATH ? ':' + env.PYTHONPATH : '');
189
+
190
+ const python = spawn(pythonCmd, [pythonScript], {
191
+ cwd: process.cwd(), stdio: ['pipe', 'pipe', 'pipe'], env
192
+ });
193
+
194
+ let output = '';
195
+ let errorOutput = '';
196
+ python.stdout.on('data', (d) => { output += d.toString(); });
197
+ python.stderr.on('data', (d) => { errorOutput += d.toString(); });
198
+
199
+ python.on('close', (code) => {
200
+ if (code !== 0) logInfo(`Python engine debug: ${errorOutput.slice(-300)}`);
201
+ try {
202
+ const lines = output.trim().split('\n').filter(l => l.length > 0);
203
+ const lastLine = lines[lines.length - 1];
204
+ resolve(JSON.parse(lastLine));
205
+ } catch (e) {
206
+ reject(new Error(`Failed to parse engine response: ${e.message}`));
207
+ }
208
+ });
209
+
210
+ python.stdin.write(JSON.stringify(command) + '\n');
211
+ python.stdin.end();
212
+ });
213
+ }
214
+
215
+ // ─── Setup & Config ──────────────────────────────────────────────
216
+
136
217
  async function runSetup() {
137
218
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
138
219
  const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
139
220
 
140
221
  try {
141
222
  console.log(BANNER);
142
- console.log(` ${colors.dim("┏━━")} ${colors.bold("PROJECT INITIALIZATION")} ${colors.dim("━━┓")}`);
143
- console.log(` ${colors.dim("┃")} ${colors.blue("Configuring your intelligence backend")} ${colors.dim("┃")}`);
144
- console.log(` ${colors.dim("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")}\n`);
145
-
223
+ logHeader("PROJECT INITIALIZATION");
224
+ log(colors.blue("Configuring your intelligence backend"));
225
+ logDivider();
226
+
146
227
  if (!fs.existsSync(path.join(process.cwd(), '.git'))) {
147
- console.log(`${colors.yellow("⚠")} ${colors.bold("Warning:")} Not in a Git repository.`);
148
- console.log(` voria works best when integrated with Git.\n`);
228
+ logWarn(`Not in a Git repository. voria works best with Git.`);
149
229
  }
150
230
 
151
- console.log(`${colors.bold("Choose LLM Provider:")}`);
231
+ console.log(`\n${colors.bold(colors.blue("Choose LLM Provider:"))}`);
152
232
  console.log(` ${colors.blue("1)")} OpenAI (GPT-4o, GPT-4 Mini)`);
153
233
  console.log(` ${colors.blue("2)")} Claude (Anthropic)`);
154
234
  console.log(` ${colors.blue("3)")} Gemini (Google)`);
@@ -163,494 +243,558 @@ async function runSetup() {
163
243
  const provider = providers[parseInt(choice) - 1] || 'openai';
164
244
 
165
245
  const apiKey = await question(`${colors.cyan(provider.toUpperCase() + " API Key")} › `);
166
-
167
- if (!apiKey) {
168
- console.log(`\n${colors.red("✖")} API key is required.`);
169
- rl.close();
170
- return;
171
- }
246
+ if (!apiKey) { logError("API key is required."); rl.close(); return; }
172
247
 
173
- console.log(`\n${colors.bold("Select Model:")}`);
174
- const models = PROVIDER_MODELS[provider] || ['default'];
175
- models.forEach((model, index) => {
176
- console.log(` ${colors.blue((index + 1) + ")")} ${model}`);
177
- });
178
-
248
+ logInfo("Fetching latest models...");
249
+ const fetchedModels = fetchModels(provider, apiKey);
250
+ const models = fetchedModels.length > 0 ? fetchedModels.map(m => m.name) : (PROVIDER_MODELS[provider] || ['gpt-4o']);
251
+
252
+ console.log(`\n${colors.bold(colors.blue("Select Model:"))}`);
253
+ models.forEach((m, i) => console.log(` ${colors.blue((i + 1) + ")")} ${m}`));
179
254
  const modelChoice = await question(`\n${colors.cyan("selection")} ${colors.dim("(1)")} › `) || '1';
180
255
  const selectedModel = models[parseInt(modelChoice) - 1] || models[0];
181
256
 
182
257
  const budget = await question(`${colors.cyan("Daily Budget ($)")} ${colors.dim("(10.0)")} › `) || '10.0';
183
258
 
184
- console.log(`\n${colors.bold("Test Framework:")}`);
259
+ console.log(`\n${colors.bold(colors.blue("Test Framework:"))}`);
185
260
  console.log(` ${colors.blue("1)")} pytest ${colors.dim("(Python)")}`);
186
261
  console.log(` ${colors.blue("2)")} Jest ${colors.dim("(JavaScript)")}`);
187
262
  console.log(` ${colors.blue("3)")} Go testing`);
188
-
189
263
  const frameworkChoice = await question(`${colors.cyan("selection")} › `) || '1';
190
264
  const frameworks = ['pytest', 'jest', 'go'];
191
265
  const framework = frameworks[parseInt(frameworkChoice) - 1] || 'pytest';
192
266
 
193
267
  const config = {
194
268
  llm_provider: provider,
195
- model: selectedModel,
269
+ llm_model: selectedModel,
196
270
  daily_budget: parseFloat(budget),
197
271
  test_framework: framework,
198
272
  created_at: new Date().toISOString()
199
273
  };
200
274
 
201
- saveConfig({ ...config, api_key: apiKey }, true);
275
+ saveConfig({ ...config, llm_api_key: apiKey }, true);
202
276
  saveConfig({ ...config, api_key_env: `${provider.toUpperCase()}_API_KEY` }, false);
203
277
 
204
- console.log(`\n${colors.green("✓")} ${colors.bold("voria initialized successfully!")}`);
205
- console.log(` ${colors.dim("Project Config:")} ${colors.blue(PROJECT_CONFIG)}`);
206
- console.log(`\n${colors.bold("Next Steps:")}`);
207
- console.log(` ${colors.blue("1.")} Run: ${colors.cyan("voria --set-github-token")}`);
208
- console.log(` ${colors.blue("2.")} Run: ${colors.cyan("voria issue owner/repo 42 --apply")}\n`);
209
-
278
+ logDivider();
279
+ logSuccess(colors.bold("voria initialized successfully!"));
280
+ log(`${colors.dim("Provider:")} ${colors.blue(provider)} | ${colors.dim("Model:")} ${colors.cyan(selectedModel)}`);
281
+ log(`${colors.dim("Config:")} ${colors.blue(CONFIG_FILE)}`);
282
+ console.log(`\n${colors.bold(colors.blue("Next Steps:"))}`);
283
+ log(`Run: ${colors.cyan("voria --set-github-token")}`);
284
+ log(`Run: ${colors.cyan("voria scan")} or ${colors.cyan("voria test hardcoded_secrets")}\n`);
210
285
  rl.close();
211
286
  } catch (error) {
212
- console.error(colors.red('Error during setup:'), error);
287
+ logError(`Setup error: ${error.message}`);
213
288
  rl.close();
214
289
  process.exit(1);
215
290
  }
216
291
  }
217
292
 
218
- // Configure existing setup
219
293
  async function runConfig() {
220
294
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
221
295
  const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
222
296
 
223
297
  try {
224
- console.log(`\n ${colors.dim("┏━━")} ${colors.bold("SETTINGS ENGINE")} ${colors.dim("━━┓")}`);
225
- console.log(` ${colors.dim("┃")} ${colors.blue("Adjusting your local parameters")} ${colors.dim("┃")}`);
226
- console.log(` ${colors.dim("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")}\n`);
227
-
298
+ logHeader("SETTINGS ENGINE");
228
299
  const config = loadConfig();
229
-
230
- console.log(` ${colors.dim("Provider:")} ${colors.blue(config.llm_provider || 'None')}`);
231
- console.log(` ${colors.dim("Budget:")} ${colors.green("$" + (config.daily_budget || '10.0'))}`);
232
- console.log(` ${colors.dim("Framework:")} ${colors.cyan(config.test_framework || 'None')}\n`);
300
+ log(`${colors.dim("Provider:")} ${colors.blue(config.llm_provider || 'None')}`);
301
+ log(`${colors.dim("Model:")} ${colors.cyan(config.llm_model || 'None')}`);
302
+ log(`${colors.dim("Budget:")} ${colors.green("$" + (config.daily_budget || '10.0'))}`);
303
+ log(`${colors.dim("Framework:")} ${colors.cyan(config.test_framework || 'None')}\n`);
233
304
 
234
305
  const change = await question(`${colors.cyan("edit")} ${colors.dim("(provider/budget/framework/none)")} › `);
235
-
236
306
  if (change === 'provider') {
237
307
  const provider = await question(`${colors.cyan("new provider")} › `);
238
308
  const apiKey = await question(`${colors.cyan("api key")} › `);
239
309
  config.llm_provider = provider;
240
- saveConfig({ ...config, api_key: apiKey }, true);
241
- console.log(`${colors.green("✓")} Provider updated`);
310
+ saveConfig({ ...config, llm_api_key: apiKey }, true);
311
+ logSuccess("Provider updated");
242
312
  } else if (change === 'budget') {
243
- const budget = await question(`${colors.cyan("new budget")} › `);
244
- config.daily_budget = parseFloat(budget);
313
+ config.daily_budget = parseFloat(await question(`${colors.cyan("new budget")} › `));
245
314
  saveConfig(config, true);
246
- console.log(`${colors.green("✓")} Budget updated`);
315
+ logSuccess("Budget updated");
247
316
  } else if (change === 'framework') {
248
- const framework = await question(`${colors.cyan("new framework")} › `);
249
- config.test_framework = framework;
317
+ config.test_framework = await question(`${colors.cyan("new framework")} › `);
250
318
  saveConfig(config, false);
251
- console.log(`${colors.green("✓")} Test framework updated`);
319
+ logSuccess("Test framework updated");
252
320
  }
321
+ logSuccess("Settings saved");
322
+ rl.close();
323
+ } catch (error) {
324
+ logError(`Config error: ${error.message}`);
325
+ rl.close();
326
+ }
327
+ }
253
328
 
254
- console.log(`\n${colors.green("✓")} Settings saved\n`);
329
+ async function setGitHubToken() {
330
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
331
+ const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
332
+ try {
333
+ logHeader("GitHub Integration");
334
+ logInfo(`Generate a token at: ${colors.blue("https://github.com/settings/tokens")}`);
335
+ logInfo(`Required scope: 'repo' (Full control of private repositories)`);
336
+ const token = await question(`\n${colors.cyan("GitHub Token")} › `);
337
+ if (!token) { logError("Token is required"); rl.close(); return; }
338
+ const config = loadConfig();
339
+ config.github_token = token;
340
+ saveConfig(config, true);
341
+ logSuccess(colors.bold("GitHub token saved successfully!"));
255
342
  rl.close();
256
343
  } catch (error) {
257
- console.error(colors.red('Error during configuration:'), error);
344
+ logError(`Error: ${error.message}`);
258
345
  rl.close();
259
346
  }
260
347
  }
261
348
 
262
- // Show status
349
+ // ─── Status & Utility ────────────────────────────────────────────
350
+
263
351
  function showStatus() {
264
- console.log(`\n${colors.bgBlue(colors.bold(" 📊 voria Status "))}`);
352
+ logHeader("voria Status");
265
353
  const config = loadConfig();
266
-
267
- if (Object.keys(config).length === 0) {
268
- console.log(`${colors.red("")} voria not initialized. Run ${colors.yellow("voria --init")}\n`);
269
- return;
270
- }
271
-
272
- console.log(`${colors.green("")} voria is configured`);
273
- console.log(` ${colors.bold("LLM Provider:")} ${colors.blue(config.llm_provider)}`);
274
- console.log(` ${colors.bold("Daily Budget:")} ${colors.green("$" + config.daily_budget)}`);
275
- console.log(` ${colors.bold("Test Framework:")} ${colors.cyan(config.test_framework)}`);
276
- console.log(` ${colors.bold("Global Config:")} ${colors.dim(CONFIG_FILE)}\n`);
354
+ if (!config.llm_provider) { logError(`Not initialized. Run ${colors.cyan("voria --init")}\n`); return; }
355
+ logSuccess("voria is configured");
356
+ log(`${colors.bold("Provider:")} ${colors.blue(config.llm_provider)}`);
357
+ log(`${colors.bold("Model:")} ${colors.cyan(config.llm_model || 'default')}`);
358
+ log(`${colors.bold("Budget:")} ${colors.green("$" + config.daily_budget)}`);
359
+ log(`${colors.bold("Framework:")} ${colors.cyan(config.test_framework)}`);
360
+ log(`${colors.bold("Config:")} ${colors.dim(CONFIG_FILE)}\n`);
277
361
  }
278
362
 
279
- // Show logs
280
363
  function showLogs() {
281
364
  const logsFile = path.join(voria_HOME, 'voria.log');
282
-
283
- if (!fs.existsSync(logsFile)) {
284
- console.log('\n📝 No logs yet\n');
285
- return;
286
- }
287
-
288
- const logs = fs.readFileSync(logsFile, 'utf-8');
289
- const lines = logs.split('\n').slice(-50);
290
-
291
- console.log('\n📝 Recent logs (last 50 lines):\n');
292
- console.log(lines.join('\n'));
365
+ if (!fs.existsSync(logsFile)) { log(colors.dim("No logs yet")); return; }
366
+ logHeader("Recent Logs");
367
+ const lines = fs.readFileSync(logsFile, 'utf-8').split('\n').slice(-50);
368
+ lines.forEach(l => log(colors.dim(l)));
293
369
  }
294
370
 
295
- // Reset configuration
296
371
  function reset() {
297
- const config = loadConfig();
298
-
299
- if (Object.keys(config).length === 0) {
300
- console.log('\n✅ No configuration to reset\n');
301
- return;
302
- }
303
-
304
372
  if (fs.existsSync(PROJECT_CONFIG)) {
305
373
  fs.unlinkSync(PROJECT_CONFIG);
306
- console.log(`✅ Removed project config: ${PROJECT_CONFIG}`);
374
+ logSuccess(`Removed project config: ${PROJECT_CONFIG}`);
307
375
  }
308
-
309
- console.log('✅ Configuration reset\n');
376
+ logSuccess("Configuration reset");
310
377
  }
311
378
 
312
- // Set GitHub token
313
- async function setGitHubToken() {
314
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
315
- const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
316
-
317
- try {
318
- console.log(`\n${colors.bgBlue(colors.bold(" 🔗 GitHub Integration "))}`);
319
- console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n`);
320
-
321
- console.log(`${colors.cyan("ℹ")} ${colors.dim("Generate a token at:")} ${colors.blue("https://github.com/settings/tokens")}`);
322
- console.log(`${colors.dim(" Required scope: 'repo' (Full control of private repositories)")}\n`);
379
+ // ─── Issue Fixing ────────────────────────────────────────────────
323
380
 
324
- const token = await question(`${colors.cyan("GitHub Token")} › `);
325
-
326
- if (!token) {
327
- console.log(`\n${colors.red("✖")} Token is required\n`);
328
- rl.close();
329
- return;
330
- }
331
-
332
- const config = loadConfig();
333
- config.github_token = token;
334
- saveConfig(config, true);
335
-
336
- console.log(`\n${colors.green("✓")} ${colors.bold("GitHub token saved successfully!")}\n`);
337
- rl.close();
338
- } catch (error) {
339
- console.error(colors.red('Error saving token:'), error);
340
- rl.close();
341
- process.exit(1);
342
- }
343
- }
344
-
345
- // List issues from a repository
346
381
  async function listIssues(repoArg, config) {
347
- // Parse repo from argument
348
382
  let owner, repo;
349
-
350
383
  if (repoArg.includes('github.com')) {
351
384
  const match = repoArg.match(/github\.com[/:]([^/]+)\/([^/\s]+)/);
352
- if (!match) {
353
- console.log(`\n${colors.red("✖")} Invalid GitHub URL`);
354
- return;
355
- }
356
- owner = match[1];
357
- repo = match[2].replace('.git', '');
385
+ if (!match) { logError("Invalid GitHub URL"); return; }
386
+ owner = match[1]; repo = match[2].replace('.git', '');
358
387
  } else if (repoArg.includes('/')) {
359
- const parts = repoArg.split('/');
360
- owner = parts[0];
361
- repo = parts[1];
362
- } else {
363
- console.log(`\n${colors.red("✖")} Invalid repository format`);
364
- return;
365
- }
388
+ [owner, repo] = repoArg.split('/');
389
+ } else { logError("Invalid repository format"); return; }
366
390
 
367
391
  const githubToken = config.github_token || process.env.GITHUB_TOKEN;
368
- if (!githubToken) {
369
- console.log(`\n${colors.red("✖")} GitHub token not found. Run ${colors.yellow("voria --set-github-token")}`);
370
- return;
371
- }
372
-
373
- console.log(`\n${colors.blue("📋")} Fetching issues from ${colors.cyan(owner + "/" + repo)}...\n`);
392
+ if (!githubToken) { logError(`GitHub token not found. Run ${colors.cyan("voria --set-github-token")}`); return; }
374
393
 
394
+ logInfo(`Fetching issues from ${colors.cyan(owner + "/" + repo)}...`);
375
395
  try {
376
- const response = await callPythonEngine({
377
- command: 'list_issues',
378
- owner: owner,
379
- repo: repo,
380
- github_token: githubToken,
381
- });
382
-
383
- if (response.status === 'error') {
384
- console.error(`${colors.red("✖")} Error: ${response.message}\n`);
385
- return;
386
- }
387
-
396
+ const response = await callPythonEngine({ command: 'list_issues', owner, repo, github_token: githubToken });
397
+ if (response.status === 'error') { logError(response.message); return; }
388
398
  const issues = response.data?.issues || [];
389
-
390
- if (issues.length === 0) {
391
- console.log(`${colors.yellow("📭")} No open issues found\n`);
392
- return;
393
- }
394
-
395
- console.log(`${colors.bgBlue(colors.bold(" 📋 Open Issues "))} ${colors.dim("in " + owner + "/" + repo + " (" + issues.length + " total)")}\n`);
396
-
399
+ if (issues.length === 0) { log(colors.dim("No open issues found")); return; }
400
+ logHeader(`Open Issues in ${owner}/${repo}`);
397
401
  for (const issue of issues) {
398
- const labels = issue.labels?.length > 0
399
- ? ` ${colors.dim("[")}${colors.yellow(issue.labels.join(", "))}${colors.dim("]")}`
400
- : '';
401
-
402
- console.log(`${colors.brightBlue("#" + issue.number)} ${colors.bold(issue.title)}${labels}`);
403
- console.log(` ${colors.dim("🔗")} ${colors.blue(issue.url)}`);
404
-
405
- if (issue.body) {
406
- const snippet = issue.body.length > 100 ? issue.body.substring(0, 100) + "..." : issue.body;
407
- console.log(` ${colors.dim("📝")} ${colors.dim(snippet.replace(/\n/g, " "))}`);
408
- }
409
- console.log('');
402
+ const labels = issue.labels?.length > 0 ? ` ${colors.yellow("[" + issue.labels.join(", ") + "]")}` : '';
403
+ log(`${colors.brightBlue("#" + issue.number)} ${colors.bold(issue.title)}${labels}`);
410
404
  }
411
-
412
- console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}`);
413
- console.log(`${colors.yellow("💡")} To fix an issue: ${colors.bold("voria issue " + owner + "/" + repo + " <number> --apply")}\n`);
414
- } catch (error) {
415
- console.error(`${colors.red("✖")} Error: ${error.message}\n`);
416
- }
405
+ logDivider();
406
+ logInfo(`To fix: ${colors.cyan("voria issue " + owner + "/" + repo + " <number> --apply")}`);
407
+ } catch (error) { logError(error.message); }
417
408
  }
418
409
 
419
- // Run plan command with LLM - Actually call Python engine
420
- // Enhanced plan command
421
410
  async function runPlanCommand(description, config, args) {
422
411
  console.log(BANNER);
423
- console.log(`${colors.bgBlue(colors.bold(" 📝 voria Plan "))} ${colors.blue("Architecting your fix")}`);
424
- console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}`);
425
- console.log(`${colors.cyan(description)}`);
426
- console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n`);
427
-
428
- console.log(`${colors.blue("⏳")} Analyzing codebase...`);
429
- console.log(`${colors.blue("⏳")} Designing solution architecture...\n`);
412
+ logHeader("voria Plan");
413
+ log(colors.cyan(description));
414
+ logDivider();
415
+ logInfo("Analyzing codebase...");
416
+ logInfo("Designing solution architecture...");
430
417
 
431
418
  try {
432
- let apiKey = config.api_key;
433
- if (!apiKey) {
434
- const envKey = `${(config.llm_provider || 'openai').toUpperCase()}_API_KEY`;
435
- apiKey = process.env[envKey];
436
- }
437
-
438
- if (!apiKey) {
439
- console.error(`${colors.red("✖")} Error: API key not found. Run ${colors.yellow("voria --config")}\n`);
440
- return;
441
- }
442
-
419
+ const apiKey = getApiKey(config);
443
420
  const response = await callPythonEngine({
444
- command: 'plan',
445
- description: description,
446
- repo_path: process.cwd(),
447
- provider: config.llm_provider || 'openai',
448
- api_key: apiKey,
449
- model: config.model,
421
+ command: 'plan', description, repo_path: process.cwd(),
422
+ provider: config.llm_provider || 'openai', api_key: apiKey, model: config.llm_model,
450
423
  });
451
-
452
- if (response.status === 'error') {
453
- console.error(`${colors.red("✖")} Error: ${response.message}\n`);
454
- return;
424
+ if (response.status === 'error') { logError(response.message); return; }
425
+ logSuccess(colors.bold("Strategic Plan Generated!"));
426
+ if (response.data?.plan) {
427
+ logHeader("Project Blueprint");
428
+ console.log(typeof response.data.plan === 'string' ? response.data.plan : JSON.stringify(response.data.plan, null, 2));
455
429
  }
430
+ if (args.includes('--dry-run')) { logInfo("Dry Run Mode — no changes made"); }
431
+ else { logInfo(`Ready to implement! Run: ${colors.cyan("voria issue <repo> <number> --apply")}`); }
432
+ } catch (error) { logError(error.message); }
433
+ }
456
434
 
457
- console.log(`${colors.green("✓")} ${colors.bold("Strategic Plan Generated!")}\n`);
458
-
459
- if (response.data && response.data.plan) {
460
- console.log(`${colors.bgBlue(colors.bold(" 📋 Project Blueprint "))}\n`);
461
- console.log(response.data.plan);
462
- console.log('\n');
463
- }
435
+ async function runIssueCommand(repo, issueNumber, config, args) {
436
+ console.log(BANNER);
437
+ logHeader("voria Issue Fix");
438
+ log(`${colors.bold("Issue:")} ${colors.brightBlue("#" + issueNumber)} in ${colors.cyan(repo)}`);
439
+ log(`${colors.bold("AI:")} ${colors.blue(config.llm_provider || 'openai')} / ${colors.cyan(config.llm_model || 'default')}`);
440
+ log(`${colors.bold("Budget:")} ${colors.green("$" + (config.daily_budget || 10.0))}/day`);
441
+ logDivider();
442
+ logInfo("Fetching issue details...");
443
+ logInfo("Analyzing code context...");
444
+ logInfo("Generating optimized fix...");
445
+
446
+ try {
447
+ const apiKey = getApiKey(config);
448
+ const githubToken = config.github_token || process.env.GITHUB_TOKEN;
449
+ if (!githubToken) { logError(`GitHub token not found. Run ${colors.cyan("voria --set-github-token")}`); return; }
450
+
451
+ const response = await callPythonEngine({
452
+ command: 'issue', issue_number: parseInt(issueNumber), repo, repo_path: process.cwd(),
453
+ provider: config.llm_provider || 'openai', api_key: apiKey, github_token: githubToken, model: config.llm_model,
454
+ });
455
+ if (response.status === 'error') { logError(response.message); return; }
456
+ logSuccess(colors.bold("Solution Generated!"));
457
+ if (response.data?.issue_title) log(`${colors.bold("Title:")} ${response.data.issue_title}`);
464
458
 
465
- const isDryRun = args.includes('--dry-run');
466
- if (isDryRun) {
467
- console.log(`${colors.cyan("🔍")} Dry Run Mode - No changes will be made\n`);
459
+ if (args.includes('--apply')) {
460
+ logInfo("Creating Pull Request...");
461
+ const prResponse = await callPythonEngine({
462
+ command: 'create_pr', owner: repo.split('/')[0], repo: repo.split('/')[1],
463
+ issue_number: parseInt(issueNumber), patch: response.data.patch, github_token: githubToken
464
+ });
465
+ if (prResponse.status === 'success') {
466
+ logSuccess(colors.bold("Pull Request Created!"));
467
+ log(`${colors.bold("URL:")} ${colors.brightBlue(prResponse.data.pr_url)}`);
468
+ log(`${colors.bold("Branch:")} ${colors.cyan(prResponse.data.branch)}`);
469
+ } else { logError(`Failed to create PR: ${prResponse.message}`); }
468
470
  } else {
469
- console.log(`${colors.green("")} Ready to implement!`);
470
- console.log(` Run: ${colors.bold("voria issue <repo> <number> --apply")}\n`);
471
+ logInfo(`Solution ready. Run with ${colors.bold("--apply")} to create PR.`);
471
472
  }
472
- } catch (error) {
473
- console.error(`${colors.red("✖")} Error: ${error.message}\n`);
474
- }
473
+ } catch (error) { logError(error.message); }
475
474
  }
476
475
 
477
- // Enhanced issue command
478
- async function runIssueCommand(repo, issueNumber, config, args) {
476
+ // ─── Tier 1: scan ────────────────────────────────────────────────
477
+
478
+ async function runScanCommand(config, args) {
479
479
  console.log(BANNER);
480
- console.log(`${colors.bgBlue(colors.bold(" 🚀 voria "))} ${colors.blue("Issue Fix Integration")}`);
481
- console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}`);
482
- console.log(`${colors.bold("🔗 Issue:")} ${colors.brightBlue("#" + issueNumber)} in ${colors.cyan(repo)}`);
483
- console.log(`${colors.bold("🤖 AI:")} ${colors.yellow(config.llm_provider || 'openai')}`);
484
- console.log(`${colors.bold("💰 Budget:")} ${colors.green("$" + (config.daily_budget || 10.0))}/day`);
485
- console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n`);
480
+ logHeader("voria Security Scan");
481
+
482
+ const categoryIdx = args.indexOf('--category');
483
+ const category = categoryIdx !== -1 && args[categoryIdx + 1] ? args[categoryIdx + 1] : 'all';
486
484
 
487
- console.log(`${colors.blue("")} Fetching issue details...`);
488
- console.log(`${colors.blue("")} Analyzing code context...`);
489
- console.log(`${colors.blue("⏳")} Generating optimized fix...\n`);
485
+ log(`${colors.bold("Category:")} ${colors.cyan(category)}`);
486
+ log(`${colors.bold("Provider:")} ${colors.blue(config.llm_provider)} / ${colors.cyan(config.llm_model || 'default')}`);
487
+ logDivider();
488
+ logInfo("Running all security tests in parallel...");
489
+ logInfo("This may take a few minutes depending on codebase size...");
490
490
 
491
491
  try {
492
- let apiKey = config.api_key;
493
- if (!apiKey) {
494
- const envKey = `${(config.llm_provider || 'openai').toUpperCase()}_API_KEY`;
495
- apiKey = process.env[envKey];
492
+ const apiKey = getApiKey(config);
493
+ const response = await callPythonEngine({
494
+ command: 'scan', provider: config.llm_provider, api_key: apiKey,
495
+ model: config.llm_model, repo_path: process.cwd(), category,
496
+ });
497
+ if (response.status === 'error') { logError(response.message); return; }
498
+
499
+ const scan = response.data?.scan || {};
500
+ logDivider();
501
+ logHeader("Scan Results");
502
+ log(`${colors.bold("Total Tests:")} ${colors.blue(scan.total_tests)}`);
503
+ log(`${colors.bold("Passed:")} ${colors.green(scan.passed)}`);
504
+ log(`${colors.bold("Failed:")} ${colors.red(scan.failed)}`);
505
+ log(`${colors.bold("Warnings:")} ${colors.yellow(scan.warnings)}`);
506
+ log(`${colors.bold("Avg Score:")} ${colors.brightBlue(scan.avg_score + "/100")}`);
507
+
508
+ if (scan.findings?.length > 0) {
509
+ logHeader("Top Findings");
510
+ scan.findings.slice(0, 10).forEach(f => {
511
+ const sev = f.severity === 'high' ? colors.red("HIGH") : f.severity === 'medium' ? colors.yellow("MED") : colors.dim("LOW");
512
+ log(` ${sev} ${colors.dim(f.file || '')} ${f.description || ''}`);
513
+ });
496
514
  }
497
-
498
- if (!apiKey) {
499
- console.error(`${colors.red("✖")} Error: API key not found. Run ${colors.yellow("voria --config")}\n`);
500
- return;
515
+ if (scan.recommendations?.length > 0) {
516
+ logHeader("Recommendations");
517
+ scan.recommendations.slice(0, 5).forEach(r => log(` ${colors.blue("")} ${r}`));
501
518
  }
519
+ logDivider();
520
+ logSuccess(`Scan complete: ${colors.blue(response.message)}`);
521
+ } catch (error) { logError(error.message); }
522
+ }
502
523
 
503
- const githubToken = config.github_token || process.env.GITHUB_TOKEN;
504
- if (!githubToken) {
505
- console.error(`${colors.red("✖")} Error: GitHub token not found. Run ${colors.yellow("voria --set-github-token")}\n`);
506
- return;
507
- }
524
+ // ─── Tier 1: test & list-tests ───────────────────────────────────
525
+
526
+ async function runTestCommand(testId, config) {
527
+ logHeader(`Running Test: ${testId}`);
528
+ logInfo(`Provider: ${colors.blue(config.llm_provider)} | Model: ${colors.cyan(config.llm_model || 'default')}`);
529
+ logDivider();
508
530
 
531
+ try {
532
+ const apiKey = getApiKey(config);
509
533
  const response = await callPythonEngine({
510
- command: 'issue',
511
- issue_number: parseInt(issueNumber),
512
- repo: repo,
513
- repo_path: process.cwd(),
514
- provider: config.llm_provider || 'openai',
515
- api_key: apiKey,
516
- github_token: githubToken,
517
- model: config.model,
534
+ command: 'test', test_id: testId, provider: config.llm_provider,
535
+ api_key: apiKey, model: config.llm_model, repo_path: process.cwd(),
518
536
  });
519
-
520
- if (response.status === 'error') {
521
- console.error(`${colors.red("✖")} Error: ${response.message}\n`);
522
- return;
537
+ if (response.status === 'error') { logError(response.message); return; }
538
+
539
+ const result = response.data?.result?.result || response.data?.result || {};
540
+ logDivider();
541
+ const status = result.status === 'passed' ? colors.green("PASSED") : result.status === 'failed' ? colors.red("FAILED") : colors.yellow("WARNING");
542
+ log(`${colors.bold("Status:")} ${status}`);
543
+ log(`${colors.bold("Score:")} ${colors.brightBlue((result.score || 0) + "/100")}`);
544
+ if (result.summary) log(`${colors.bold("Summary:")} ${result.summary}`);
545
+ if (result.findings?.length > 0) {
546
+ result.findings.slice(0, 5).forEach(f => {
547
+ log(` ${colors.yellow("→")} ${f.description || JSON.stringify(f)}`);
548
+ });
523
549
  }
524
-
525
- console.log(`${colors.green("")} ${colors.bold("Solution Generated!")}\n`);
526
-
527
- if (response.data) {
528
- console.log(`${colors.bold("📝 Title:")} ${response.data.issue_title}`);
550
+ if (result.recommendations?.length > 0) {
551
+ result.recommendations.forEach(r => log(` ${colors.blue("💡")} ${r}`));
529
552
  }
553
+ logDivider();
554
+ } catch (error) { logError(error.message); }
555
+ }
530
556
 
531
- const shouldApply = args.includes('--apply');
532
- if (shouldApply) {
533
- console.log(`\n${colors.blue("⏳")} Creating Pull Request...`);
534
- const prResponse = await callPythonEngine({
535
- command: 'create_pr',
536
- owner: repo.split('/')[0],
537
- repo: repo.split('/')[1],
538
- issue_number: parseInt(issueNumber),
539
- patch: response.data.patch,
540
- github_token: githubToken
557
+ async function runListTests(config) {
558
+ logHeader("Available Tests");
559
+ try {
560
+ const response = await callPythonEngine({ command: 'list_tests' });
561
+ if (response.status === 'error') { logError(response.message); return; }
562
+ const tests = response.data?.tests || [];
563
+ const categories = {};
564
+ tests.forEach(t => {
565
+ if (!categories[t.category]) categories[t.category] = [];
566
+ categories[t.category].push(t);
567
+ });
568
+ for (const [cat, items] of Object.entries(categories)) {
569
+ console.log(`\n ${colors.bold(colors.blue(cat.toUpperCase()))}`);
570
+ items.forEach(t => {
571
+ log(` ${colors.cyan(t.id.padEnd(25))} ${t.name} ${colors.dim("— " + (t.description || '').slice(0, 50))}`);
541
572
  });
573
+ }
574
+ logDivider();
575
+ logInfo(`Run a test: ${colors.cyan("voria test <test_id>")}`);
576
+ logInfo(`Full scan: ${colors.cyan("voria scan")}`);
577
+ } catch (error) { logError(error.message); }
578
+ }
542
579
 
543
- if (prResponse.status === 'success') {
544
- console.log(`\n${colors.green("🎉")} ${colors.bold("Pull Request Created!")}`);
545
- console.log(`${colors.bold("🔗 URL:")} ${colors.brightBlue(prResponse.data.pr_url)}`);
546
- console.log(`${colors.bold("🌿 Branch:")} ${colors.cyan(prResponse.data.branch)}\n`);
547
- } else {
548
- console.error(`${colors.red("✖")} Failed to create PR: ${prResponse.message}\n`);
580
+ // ─── Tier 1: watch ───────────────────────────────────────────────
581
+
582
+ async function runWatchCommand(config) {
583
+ logHeader("voria Watch Mode");
584
+ logInfo("Monitoring source files for changes...");
585
+ logInfo("Press Ctrl+C to stop\n");
586
+
587
+ const extensions = new Set(['.py', '.js', '.ts', '.go', '.rs', '.java']);
588
+ const ignore = ['node_modules', '.git', 'venv', '__pycache__', 'target'];
589
+
590
+ function getSnapshot(dir) {
591
+ const snap = {};
592
+ try {
593
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
594
+ for (const e of entries) {
595
+ if (ignore.some(i => e.name.includes(i))) continue;
596
+ const fp = path.join(dir, e.name);
597
+ if (e.isDirectory()) Object.assign(snap, getSnapshot(fp));
598
+ else if (extensions.has(path.extname(e.name))) {
599
+ try { snap[fp] = fs.statSync(fp).mtimeMs; } catch {}
600
+ }
549
601
  }
550
- } else {
551
- console.log(`\n${colors.yellow("💡")} Solution ready for review.`);
552
- console.log(` Run with ${colors.bold("--apply")} to automatically create a PR.\n`);
553
- }
554
- } catch (error) {
555
- console.error(`${colors.red("✖")} Error: ${error.message}\n`);
602
+ } catch {}
603
+ return snap;
556
604
  }
605
+
606
+ let prev = getSnapshot(process.cwd());
607
+ logInfo(`Watching ${Object.keys(prev).length} files...`);
608
+
609
+ const testIds = ['hardcoded_secrets', 'xss', 'sql_injection'];
610
+ const apiKey = getApiKey(config);
611
+
612
+ const interval = setInterval(async () => {
613
+ const curr = getSnapshot(process.cwd());
614
+ const changed = [];
615
+ for (const [fp, mtime] of Object.entries(curr)) {
616
+ if (!prev[fp] || prev[fp] !== mtime) changed.push(fp);
617
+ }
618
+ if (changed.length > 0) {
619
+ logInfo(`${colors.yellow(changed.length + " file(s) changed:")} ${changed.map(f => path.basename(f)).join(', ')}`);
620
+ for (const tid of testIds) {
621
+ logInfo(`Re-running test: ${colors.cyan(tid)}...`);
622
+ try {
623
+ const r = await callPythonEngine({
624
+ command: 'test', test_id: tid, provider: config.llm_provider,
625
+ api_key: apiKey, model: config.llm_model, repo_path: process.cwd(),
626
+ });
627
+ const st = r.data?.result?.result?.status || r.data?.result?.status || 'unknown';
628
+ const icon = st === 'passed' ? colors.green("✓") : st === 'failed' ? colors.red("✖") : colors.yellow("⚠");
629
+ log(` ${icon} ${tid}: ${st}`);
630
+ } catch (e) { logError(`${tid}: ${e.message}`); }
631
+ }
632
+ prev = curr;
633
+ }
634
+ }, 2000);
635
+
636
+ process.on('SIGINT', () => { clearInterval(interval); logInfo("Watch mode stopped."); process.exit(0); });
637
+ // Keep alive
638
+ await new Promise(() => {});
557
639
  }
558
640
 
559
- // Helper function to call Python engine
560
- async function callPythonEngine(command) {
561
- const { spawn } = await import('child_process');
562
- const pathMod = await import('path');
563
- const fsMod = await import('fs');
564
-
565
- return new Promise((resolve, reject) => {
566
- const pythonScript = pathMod.default.join(__dirname, '..', 'python', 'voria', 'engine.py');
567
- const pythonPath = pathMod.default.join(__dirname, '..', 'python');
568
-
569
- // Check if virtual environment exists
570
- const venvPath = pathMod.default.join(__dirname, '..', 'venv', 'bin', 'python');
571
- const pythonCmd = fsMod.default.existsSync(venvPath) ? venvPath : 'python3';
572
-
573
- // Add python directory to PYTHONPATH so voria module can be found
574
- const env = { ...process.env };
575
- const currentPath = env.PYTHONPATH || '';
576
- env.PYTHONPATH = pythonPath + (currentPath ? ':' + currentPath : '');
577
-
578
- const python = spawn(pythonCmd, [pythonScript], {
579
- cwd: process.cwd(),
580
- stdio: ['pipe', 'pipe', 'pipe'],
581
- env: env
582
- });
641
+ // ─── Tier 1: diff ────────────────────────────────────────────────
583
642
 
584
- let output = '';
585
- let errorOutput = '';
643
+ async function runDiffCommand(args, config) {
644
+ const refA = args[1] || 'HEAD~1';
645
+ const refB = args[2] || 'HEAD';
646
+ logHeader("voria Security Diff");
647
+ log(`${colors.bold("Comparing:")} ${colors.cyan(refA)} → ${colors.cyan(refB)}`);
648
+ logDivider();
586
649
 
587
- python.stdout.on('data', (data) => {
588
- output += data.toString();
650
+ try {
651
+ const response = await callPythonEngine({
652
+ command: 'diff', ref_a: refA, ref_b: refB, repo_path: process.cwd(),
589
653
  });
654
+ if (response.status === 'error') { logError(response.message); return; }
655
+ const diff = response.data?.diff || {};
656
+ log(`${colors.bold("Files Changed:")} ${colors.blue(diff.total_changed)}`);
657
+ log(`${colors.bold("Risk Level:")} ${diff.risk_level === 'critical' ? colors.red(diff.risk_level) : diff.risk_level === 'elevated' ? colors.yellow(diff.risk_level) : colors.green(diff.risk_level)}`);
658
+ if (diff.high_risk_files?.length > 0) {
659
+ log(`${colors.bold(colors.red("High Risk:"))}`);
660
+ diff.high_risk_files.forEach(f => log(` ${colors.red("●")} ${f}`));
661
+ }
662
+ if (diff.medium_risk_files?.length > 0) {
663
+ log(`${colors.bold(colors.yellow("Medium Risk:"))}`);
664
+ diff.medium_risk_files.forEach(f => log(` ${colors.yellow("●")} ${f}`));
665
+ }
666
+ if (diff.recommendation) log(`\n${colors.blue("💡")} ${diff.recommendation}`);
667
+ logDivider();
668
+ } catch (error) { logError(error.message); }
669
+ }
670
+
671
+ // ─── Tier 1: benchmark ──────────────────────────────────────────
672
+
673
+ async function runBenchmarkCommand(args) {
674
+ const url = args[1];
675
+ if (!url) { logError("URL required. Usage: voria benchmark <URL>"); return; }
590
676
 
591
- python.stderr.on('data', (data) => {
592
- errorOutput += data.toString();
677
+ const reqIdx = args.indexOf('--requests');
678
+ const conIdx = args.indexOf('--concurrency');
679
+ const requests = reqIdx !== -1 ? parseInt(args[reqIdx + 1]) : 100;
680
+ const concurrency = conIdx !== -1 ? parseInt(args[conIdx + 1]) : 10;
681
+
682
+ logHeader("voria HTTP Benchmark");
683
+ log(`${colors.bold("Target:")} ${colors.cyan(url)}`);
684
+ log(`${colors.bold("Requests:")} ${colors.blue(requests)}`);
685
+ log(`${colors.bold("Concurrency:")} ${colors.blue(concurrency)}`);
686
+ logDivider();
687
+ logInfo("Sending requests...");
688
+
689
+ try {
690
+ const response = await callPythonEngine({
691
+ command: 'benchmark', url, requests, concurrency,
593
692
  });
693
+ if (response.status === 'error') { logError(response.message); return; }
694
+ const bm = response.data?.benchmark || {};
695
+ logHeader("Benchmark Results");
696
+ log(`${colors.bold("Requests/sec:")} ${colors.green(bm.rps)}`);
697
+ log(`${colors.bold("Total Time:")} ${colors.blue(bm.total_time_sec + "s")}`);
698
+ log(`${colors.bold("Success:")} ${colors.green(bm.successful)} | ${colors.bold("Failed:")} ${colors.red(bm.failed)}`);
699
+ logDivider();
700
+ log(`${colors.bold("Latency (ms):")}`);
701
+ log(` ${colors.dim("avg:")} ${colors.blue(bm.latency_avg_ms)}`);
702
+ log(` ${colors.dim("p50:")} ${colors.green(bm.latency_p50_ms)}`);
703
+ log(` ${colors.dim("p95:")} ${colors.yellow(bm.latency_p95_ms)}`);
704
+ log(` ${colors.dim("p99:")} ${colors.red(bm.latency_p99_ms)}`);
705
+ log(` ${colors.dim("min:")} ${colors.dim(bm.latency_min_ms)} | ${colors.dim("max:")} ${colors.dim(bm.latency_max_ms)}`);
706
+ logDivider();
707
+ } catch (error) { logError(error.message); }
708
+ }
594
709
 
595
- python.on('close', (code) => {
596
- if (code !== 0) {
597
- console.debug(`Python engine exit code: ${code}`);
598
- console.debug(`Error output: ${errorOutput}`);
599
- }
600
-
601
- try {
602
- // Parse NDJSON responses
603
- const lines = output.trim().split('\n').filter(l => l.length > 0);
604
- const lastLine = lines[lines.length - 1];
605
- const response = JSON.parse(lastLine);
606
- resolve(response);
607
- } catch (e) {
608
- reject(new Error(`Failed to parse Python response: ${e.message}`));
609
- }
710
+ // ─── Tier 1: ci ──────────────────────────────────────────────────
711
+
712
+ async function runCiCommand(config) {
713
+ logHeader("voria CI/CD Scan");
714
+ logInfo("Running security tests and generating SARIF...");
715
+
716
+ try {
717
+ const apiKey = getApiKey(config);
718
+ const response = await callPythonEngine({
719
+ command: 'ci', provider: config.llm_provider, api_key: apiKey,
720
+ model: config.llm_model, repo_path: process.cwd(),
610
721
  });
722
+ if (response.status === 'error') { logError(response.message); return; }
723
+
724
+ const sarif = response.data?.sarif;
725
+ if (sarif) {
726
+ const outPath = path.join(process.cwd(), 'voria-results.sarif');
727
+ fs.writeFileSync(outPath, JSON.stringify(sarif, null, 2));
728
+ logSuccess(`SARIF report written to: ${colors.cyan(outPath)}`);
729
+ log(`${colors.bold("Findings:")} ${colors.blue(response.data.findings_count)}`);
730
+ logInfo(`Upload to GitHub: ${colors.cyan("gh api repos/{owner}/{repo}/code-scanning/sarifs -f sarif=@voria-results.sarif")}`);
731
+ }
732
+ logDivider();
733
+ } catch (error) { logError(error.message); }
734
+ }
611
735
 
612
- // Send command to Python engine
613
- python.stdin.write(JSON.stringify(command) + '\n');
614
- python.stdin.end();
615
- });
736
+ // ─── Tier 1: fix --auto ──────────────────────────────────────────
737
+
738
+ async function runFixAutoCommand(args, config) {
739
+ const repo = args[1];
740
+ const issueNumber = args[2];
741
+ if (!repo || !issueNumber) {
742
+ logError("Usage: voria fix --auto <owner/repo> <issue_number>");
743
+ return;
744
+ }
745
+ logHeader("voria Auto-Fix");
746
+ log(`${colors.bold("Issue:")} ${colors.brightBlue("#" + issueNumber)} in ${colors.cyan(repo)}`);
747
+ logDivider();
748
+ logInfo("Generating fix, auto-applying, and running tests...");
749
+
750
+ try {
751
+ const apiKey = getApiKey(config);
752
+ const githubToken = config.github_token || process.env.GITHUB_TOKEN;
753
+ if (!githubToken) { logError("GitHub token required."); return; }
754
+
755
+ const response = await callPythonEngine({
756
+ command: 'issue', issue_number: parseInt(issueNumber), repo, repo_path: process.cwd(),
757
+ provider: config.llm_provider, api_key: apiKey, github_token: githubToken, model: config.llm_model, auto: true,
758
+ });
759
+ if (response.status === 'error') { logError(response.message); return; }
760
+ logSuccess("Patch generated and auto-applied!");
761
+ if (response.data?.files_modified) {
762
+ log(`${colors.bold("Files modified:")} ${response.data.files_modified.length}`);
763
+ }
764
+ logInfo("Running tests to verify fix...");
765
+ try {
766
+ const testResult = await callPythonEngine({
767
+ command: 'test', test_id: 'hardcoded_secrets', provider: config.llm_provider,
768
+ api_key: apiKey, model: config.llm_model, repo_path: process.cwd(),
769
+ });
770
+ logSuccess(`Post-fix test: ${testResult.data?.result?.result?.status || 'completed'}`);
771
+ } catch (e) { logWarn(`Post-fix test skipped: ${e.message}`); }
772
+ logDivider();
773
+ } catch (error) { logError(error.message); }
616
774
  }
617
775
 
618
- // Helper function to call Rust core
776
+ // ─── Rust Core ───────────────────────────────────────────────────
777
+
619
778
  async function runRustCore(args) {
620
- const { spawn } = await import('child_process');
621
- const pathMod = await import('path');
622
- const fsMod = await import('fs');
623
-
624
779
  return new Promise((resolve, reject) => {
625
- // Try to find the rust binary
626
780
  const possiblePaths = [
627
- pathMod.default.join(__dirname, '..', 'target', 'release', 'voria'),
628
- pathMod.default.join(__dirname, '..', 'target', 'debug', 'voria'),
629
- pathMod.default.join(__dirname, '..', 'rust', 'target', 'release', 'voria'),
630
- pathMod.default.join(__dirname, '..', 'rust', 'target', 'debug', 'voria'),
781
+ path.join(__dirname, '..', 'target', 'release', 'voria'),
782
+ path.join(__dirname, '..', 'target', 'debug', 'voria'),
783
+ path.join(__dirname, '..', 'rust', 'target', 'release', 'voria'),
784
+ path.join(__dirname, '..', 'rust', 'target', 'debug', 'voria'),
631
785
  ];
632
-
633
- let rustBin = possiblePaths.find(p => fsMod.default.existsSync(p));
634
-
786
+ let rustBin = possiblePaths.find(p => fs.existsSync(p));
635
787
  if (!rustBin) {
636
- // Check if it's in the system path
637
- try {
638
- execSync('voria --version', { stdio: 'ignore' });
639
- rustBin = 'voria';
640
- } catch (e) {
641
- return reject(new Error('Rust core binary (voria) not found. Please run "cargo build --release" in the rust directory.'));
642
- }
643
- }
644
-
788
+ try { execSync('voria --version', { stdio: 'ignore' }); rustBin = 'voria'; }
789
+ catch (e) { return reject(new Error('Rust core binary not found.')); }
790
+ }
645
791
  const child = spawn(rustBin, args, { stdio: 'inherit' });
646
- child.on('exit', (code) => {
647
- if (code === 0) resolve();
648
- else reject(new Error(`Rust core exited with code ${code}`));
649
- });
792
+ child.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`Exit code ${code}`)));
650
793
  });
651
794
  }
652
795
 
653
- // Main CLI router
796
+ // ─── Main CLI Router ─────────────────────────────────────────────
797
+
654
798
  async function main() {
655
799
  const args = process.argv.slice(2);
656
800
 
@@ -663,8 +807,7 @@ async function main() {
663
807
 
664
808
  switch (command) {
665
809
  case '--version':
666
- case '-v':
667
- console.log(`voria v${VERSION}`);
810
+ console.log(`${colors.blue("voria")} v${VERSION}`);
668
811
  break;
669
812
 
670
813
  case '--init':
@@ -681,14 +824,8 @@ async function main() {
681
824
 
682
825
  case '--list-issues': {
683
826
  const config = loadConfig();
684
- const repoArg = args[1];
685
- if (!repoArg) {
686
- console.log('\n❌ Please provide a repository');
687
- console.log(' Example: voria --list-issues owner/repo');
688
- console.log(' Or: voria --list-issues https://github.com/owner/repo\n');
689
- process.exit(1);
690
- }
691
- await listIssues(repoArg, config);
827
+ if (!args[1]) { logError("Provide a repo. Example: voria --list-issues owner/repo"); process.exit(1); }
828
+ await listIssues(args[1], config);
692
829
  break;
693
830
  }
694
831
 
@@ -705,102 +842,119 @@ async function main() {
705
842
  break;
706
843
 
707
844
  case 'plan': {
708
- const config = loadConfig();
709
- if (Object.keys(config).length === 0) {
710
- console.log('\n❌ voria not initialized');
711
- console.log(' Run: voria --init\n');
712
- process.exit(1);
713
- }
714
- const description = args.slice(1).join(' ');
715
- if (!description) {
716
- console.log('\n❌ Please provide a description');
717
- console.log(' Example: voria plan "Remove deprecated login() method"\n');
718
- process.exit(1);
719
- }
720
- await runPlanCommand(description, config, args);
845
+ const config = requireInit();
846
+ const desc = args.slice(1).filter(a => !a.startsWith('--')).join(' ');
847
+ if (!desc) { logError('Provide a description. Example: voria plan "Remove deprecated method"'); process.exit(1); }
848
+ await runPlanCommand(desc, config, args);
721
849
  break;
722
850
  }
723
851
 
724
852
  case 'issue': {
725
- const config = loadConfig();
726
- if (Object.keys(config).length === 0) {
727
- console.log('\n❌ voria not initialized');
728
- console.log(' Run: voria --init\n');
729
- process.exit(1);
730
- }
731
-
732
- // Parse: voria issue owner/repo issue_number
733
- const issueArgs = args.slice(1);
734
- if (issueArgs.length < 2) {
735
- console.log('\n❌ Please provide repository and issue number');
736
- console.log(' Example: voria issue owner/repo 42\n');
737
- process.exit(1);
738
- }
739
-
740
- const repo = issueArgs[0];
741
- const issueNumber = issueArgs[1];
742
-
743
- await runIssueCommand(repo, issueNumber, config, args);
853
+ const config = requireInit();
854
+ if (args.length < 3) { logError("Usage: voria issue owner/repo 42"); process.exit(1); }
855
+ await runIssueCommand(args[1], args[2], config, args);
744
856
  break;
745
857
  }
746
858
 
747
859
  case 'apply': {
748
- const config = loadConfig();
749
- if (Object.keys(config).length === 0) {
750
- console.log('\n❌ voria not initialized');
751
- console.log(' Run: voria --init\n');
752
- process.exit(1);
753
- }
860
+ requireInit();
754
861
  const patchFile = args[1];
755
- if (!patchFile) {
756
- console.log('\n❌ Please provide a patch file');
757
- console.log(' Example: voria apply fix.patch\n');
758
- process.exit(1);
759
- }
760
-
761
- if (!fs.existsSync(patchFile)) {
762
- console.log(`\n❌ Patch file not found: ${patchFile}\n`);
763
- process.exit(1);
862
+ if (!patchFile) { logError("Provide a patch file. Example: voria apply fix.patch"); process.exit(1); }
863
+ if (!fs.existsSync(patchFile)) { logError(`Patch file not found: ${patchFile}`); process.exit(1); }
864
+ logInfo(`Applying patch: ${patchFile}`);
865
+ try {
866
+ const response = await callPythonEngine({ command: 'apply', patch: fs.readFileSync(patchFile, 'utf-8'), repo_path: process.cwd() });
867
+ response.status === 'error' ? logError(response.message) : logSuccess(response.message);
868
+ } catch (error) { logError(error.message); }
869
+ break;
870
+ }
871
+
872
+ // ─── Tier 1 Commands ─────────────────────────────
873
+
874
+ case 'scan': {
875
+ const config = requireInit();
876
+ await runScanCommand(config, args);
877
+ break;
878
+ }
879
+
880
+ case 'test': {
881
+ const config = requireInit();
882
+ const testId = args[1];
883
+ if (!testId) { logError("Provide a test ID. Run 'voria list-tests' to see available tests."); process.exit(1); }
884
+ await runTestCommand(testId, config);
885
+ break;
886
+ }
887
+
888
+ case 'list-tests': {
889
+ const config = loadConfig();
890
+ await runListTests(config);
891
+ break;
892
+ }
893
+
894
+ case 'watch': {
895
+ const config = requireInit();
896
+ await runWatchCommand(config);
897
+ break;
898
+ }
899
+
900
+ case 'diff': {
901
+ const config = loadConfig();
902
+ await runDiffCommand(args, config);
903
+ break;
904
+ }
905
+
906
+ case 'benchmark': {
907
+ await runBenchmarkCommand(args);
908
+ break;
909
+ }
910
+
911
+ case 'ci': {
912
+ const config = requireInit();
913
+ await runCiCommand(config);
914
+ break;
915
+ }
916
+
917
+ case 'fix': {
918
+ const config = requireInit();
919
+ if (args.includes('--auto')) {
920
+ const fixArgs = args.filter(a => a !== 'fix' && a !== '--auto');
921
+ await runFixAutoCommand(fixArgs.length >= 2 ? fixArgs : args.slice(1), config);
922
+ } else {
923
+ logError("Usage: voria fix --auto <owner/repo> <issue_number>");
764
924
  }
925
+ break;
926
+ }
765
927
 
766
- const patch = fs.readFileSync(patchFile, 'utf-8');
767
- console.log(`\n🔧 Applying patch: ${patchFile}`);
768
-
928
+ case 'token': {
929
+ requireInit();
930
+ logHeader("Token Usage");
769
931
  try {
770
- const response = await callPythonEngine({
771
- command: 'apply',
772
- patch: patch,
773
- repo_path: process.cwd()
774
- });
775
-
776
- if (response.status === 'error') {
777
- console.error(`❌ Error: ${response.message}\n`);
778
- } else {
779
- console.log(`✅ ${response.message}\n`);
780
- }
781
- } catch (error) {
782
- console.error(`❌ Error: ${error.message}\n`);
783
- }
932
+ const response = await callPythonEngine({ command: 'token', subcommand: args[1] || 'info' });
933
+ if (response.data?.tokens) {
934
+ const t = response.data.tokens;
935
+ log(`${colors.bold("Used:")} ${colors.blue(t.used || 0)} tokens`);
936
+ log(`${colors.bold("Cost:")} ${colors.green("$" + (t.cost || 0))}`);
937
+ } else { logInfo(response.message); }
938
+ } catch (error) { logError(error.message); }
784
939
  break;
785
940
  }
786
941
 
787
942
  case '--graph':
788
- try {
789
- await runRustCore(['--graph']);
790
- } catch (error) {
791
- console.error(`${colors.red("")} Error running analysis: ${error.message}`);
792
- console.log(`${colors.yellow("💡")} Make sure to build the rust core: ${colors.bold("cargo build --release")}`);
943
+ try { await runRustCore(['--graph']); }
944
+ catch (error) {
945
+ logError(`Error running analysis: ${error.message}`);
946
+ logInfo(`Build rust core: ${colors.cyan("cargo build --release")}`);
793
947
  }
794
948
  break;
795
949
 
796
950
  default:
797
- console.log(`\n❌ Unknown command: ${command}`);
798
- console.log(' Run: voria --help\n');
951
+ logError(`Unknown command: ${colors.bold(command)}`);
952
+ logInfo(`Run ${colors.cyan("voria --help")} for usage.`);
799
953
  process.exit(1);
800
954
  }
801
955
  }
802
956
 
803
957
  main().catch(error => {
804
- console.error('Error:', error);
958
+ logError(error.message);
805
959
  process.exit(1);
806
960
  });