@voria/cli 0.0.4 → 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 +625 -486
  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 +58 -16
  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,114 +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
- /**
135
- * Dynamically fetch models for a provider using Python backend
136
- */
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
+
137
171
  function fetchModels(provider, apiKey) {
138
172
  try {
139
173
  const scriptPath = path.join(__dirname, '..', 'python', 'voria', 'core', 'llm', 'model_discovery.py');
140
174
  const output = execSync(`python3 "${scriptPath}" "${provider}" "${apiKey}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
141
175
  return JSON.parse(output);
142
- } catch (e) {
143
- return [];
144
- }
176
+ } catch (e) { return []; }
145
177
  }
146
178
 
147
- // Interactive setup
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
+
148
217
  async function runSetup() {
149
218
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
150
219
  const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
151
220
 
152
221
  try {
153
222
  console.log(BANNER);
154
- console.log(` ${colors.dim("┏━━")} ${colors.bold("PROJECT INITIALIZATION")} ${colors.dim("━━┓")}`);
155
- console.log(` ${colors.dim("┃")} ${colors.blue("Configuring your intelligence backend")} ${colors.dim("┃")}`);
156
- console.log(` ${colors.dim("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")}\n`);
157
-
223
+ logHeader("PROJECT INITIALIZATION");
224
+ log(colors.blue("Configuring your intelligence backend"));
225
+ logDivider();
226
+
158
227
  if (!fs.existsSync(path.join(process.cwd(), '.git'))) {
159
- console.log(`${colors.yellow("⚠")} ${colors.bold("Warning:")} Not in a Git repository.`);
160
- console.log(` voria works best when integrated with Git.\n`);
228
+ logWarn(`Not in a Git repository. voria works best with Git.`);
161
229
  }
162
230
 
163
- console.log(`${colors.bold("Choose LLM Provider:")}`);
231
+ console.log(`\n${colors.bold(colors.blue("Choose LLM Provider:"))}`);
164
232
  console.log(` ${colors.blue("1)")} OpenAI (GPT-4o, GPT-4 Mini)`);
165
233
  console.log(` ${colors.blue("2)")} Claude (Anthropic)`);
166
234
  console.log(` ${colors.blue("3)")} Gemini (Google)`);
@@ -175,497 +243,558 @@ async function runSetup() {
175
243
  const provider = providers[parseInt(choice) - 1] || 'openai';
176
244
 
177
245
  const apiKey = await question(`${colors.cyan(provider.toUpperCase() + " API Key")} › `);
178
-
179
- if (!apiKey) {
180
- console.log(`\n${colors.red("✖")} API key is required.`);
181
- rl.close();
182
- return;
183
- }
246
+ if (!apiKey) { logError("API key is required."); rl.close(); return; }
184
247
 
185
- console.log(`\n${colors.dim(" Fetching latest models...")}`);
248
+ logInfo("Fetching latest models...");
186
249
  const fetchedModels = fetchModels(provider, apiKey);
187
250
  const models = fetchedModels.length > 0 ? fetchedModels.map(m => m.name) : (PROVIDER_MODELS[provider] || ['gpt-4o']);
188
251
 
189
- console.log(`\n${colors.bold("Select Model:")}`);
190
- models.forEach((model, index) => {
191
- console.log(` ${colors.blue((index + 1) + ")")} ${model}`);
192
- });
193
-
252
+ console.log(`\n${colors.bold(colors.blue("Select Model:"))}`);
253
+ models.forEach((m, i) => console.log(` ${colors.blue((i + 1) + ")")} ${m}`));
194
254
  const modelChoice = await question(`\n${colors.cyan("selection")} ${colors.dim("(1)")} › `) || '1';
195
255
  const selectedModel = models[parseInt(modelChoice) - 1] || models[0];
196
256
 
197
257
  const budget = await question(`${colors.cyan("Daily Budget ($)")} ${colors.dim("(10.0)")} › `) || '10.0';
198
258
 
199
- console.log(`\n${colors.bold("Test Framework:")}`);
259
+ console.log(`\n${colors.bold(colors.blue("Test Framework:"))}`);
200
260
  console.log(` ${colors.blue("1)")} pytest ${colors.dim("(Python)")}`);
201
261
  console.log(` ${colors.blue("2)")} Jest ${colors.dim("(JavaScript)")}`);
202
262
  console.log(` ${colors.blue("3)")} Go testing`);
203
-
204
263
  const frameworkChoice = await question(`${colors.cyan("selection")} › `) || '1';
205
264
  const frameworks = ['pytest', 'jest', 'go'];
206
265
  const framework = frameworks[parseInt(frameworkChoice) - 1] || 'pytest';
207
266
 
208
267
  const config = {
209
268
  llm_provider: provider,
210
- model: selectedModel,
269
+ llm_model: selectedModel,
211
270
  daily_budget: parseFloat(budget),
212
271
  test_framework: framework,
213
272
  created_at: new Date().toISOString()
214
273
  };
215
274
 
216
- saveConfig({ ...config, api_key: apiKey }, true);
275
+ saveConfig({ ...config, llm_api_key: apiKey }, true);
217
276
  saveConfig({ ...config, api_key_env: `${provider.toUpperCase()}_API_KEY` }, false);
218
277
 
219
- console.log(`\n${colors.green("✓")} ${colors.bold("voria initialized successfully!")}`);
220
- console.log(` ${colors.dim("Project Config:")} ${colors.blue(PROJECT_CONFIG)}`);
221
- console.log(`\n${colors.bold("Next Steps:")}`);
222
- console.log(` ${colors.blue("1.")} Run: ${colors.cyan("voria --set-github-token")}`);
223
- console.log(` ${colors.blue("2.")} Run: ${colors.cyan("voria issue owner/repo 42 --apply")}\n`);
224
-
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`);
225
285
  rl.close();
226
286
  } catch (error) {
227
- console.error(colors.red('Error during setup:'), error);
287
+ logError(`Setup error: ${error.message}`);
228
288
  rl.close();
229
289
  process.exit(1);
230
290
  }
231
291
  }
232
292
 
233
- // Configure existing setup
234
293
  async function runConfig() {
235
294
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
236
295
  const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
237
296
 
238
297
  try {
239
- console.log(`\n ${colors.dim("┏━━")} ${colors.bold("SETTINGS ENGINE")} ${colors.dim("━━┓")}`);
240
- console.log(` ${colors.dim("┃")} ${colors.blue("Adjusting your local parameters")} ${colors.dim("┃")}`);
241
- console.log(` ${colors.dim("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")}\n`);
242
-
298
+ logHeader("SETTINGS ENGINE");
243
299
  const config = loadConfig();
244
-
245
- console.log(` ${colors.dim("Provider:")} ${colors.blue(config.llm_provider || 'None')}`);
246
- console.log(` ${colors.dim("Budget:")} ${colors.green("$" + (config.daily_budget || '10.0'))}`);
247
- 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`);
248
304
 
249
305
  const change = await question(`${colors.cyan("edit")} ${colors.dim("(provider/budget/framework/none)")} › `);
250
-
251
306
  if (change === 'provider') {
252
307
  const provider = await question(`${colors.cyan("new provider")} › `);
253
308
  const apiKey = await question(`${colors.cyan("api key")} › `);
254
309
  config.llm_provider = provider;
255
- saveConfig({ ...config, api_key: apiKey }, true);
256
- console.log(`${colors.green("✓")} Provider updated`);
310
+ saveConfig({ ...config, llm_api_key: apiKey }, true);
311
+ logSuccess("Provider updated");
257
312
  } else if (change === 'budget') {
258
- const budget = await question(`${colors.cyan("new budget")} › `);
259
- config.daily_budget = parseFloat(budget);
313
+ config.daily_budget = parseFloat(await question(`${colors.cyan("new budget")} › `));
260
314
  saveConfig(config, true);
261
- console.log(`${colors.green("✓")} Budget updated`);
315
+ logSuccess("Budget updated");
262
316
  } else if (change === 'framework') {
263
- const framework = await question(`${colors.cyan("new framework")} › `);
264
- config.test_framework = framework;
317
+ config.test_framework = await question(`${colors.cyan("new framework")} › `);
265
318
  saveConfig(config, false);
266
- console.log(`${colors.green("✓")} Test framework updated`);
319
+ logSuccess("Test framework updated");
267
320
  }
321
+ logSuccess("Settings saved");
322
+ rl.close();
323
+ } catch (error) {
324
+ logError(`Config error: ${error.message}`);
325
+ rl.close();
326
+ }
327
+ }
268
328
 
269
- 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!"));
270
342
  rl.close();
271
343
  } catch (error) {
272
- console.error(colors.red('Error during configuration:'), error);
344
+ logError(`Error: ${error.message}`);
273
345
  rl.close();
274
346
  }
275
347
  }
276
348
 
277
- // Show status
349
+ // ─── Status & Utility ────────────────────────────────────────────
350
+
278
351
  function showStatus() {
279
- console.log(`\n${colors.bgBlue(colors.bold(" 📊 voria Status "))}`);
352
+ logHeader("voria Status");
280
353
  const config = loadConfig();
281
-
282
- if (Object.keys(config).length === 0) {
283
- console.log(`${colors.red("")} voria not initialized. Run ${colors.yellow("voria --init")}\n`);
284
- return;
285
- }
286
-
287
- console.log(`${colors.green("")} voria is configured`);
288
- console.log(` ${colors.bold("LLM Provider:")} ${colors.blue(config.llm_provider)}`);
289
- console.log(` ${colors.bold("Daily Budget:")} ${colors.green("$" + config.daily_budget)}`);
290
- console.log(` ${colors.bold("Test Framework:")} ${colors.cyan(config.test_framework)}`);
291
- 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`);
292
361
  }
293
362
 
294
- // Show logs
295
363
  function showLogs() {
296
364
  const logsFile = path.join(voria_HOME, 'voria.log');
297
-
298
- if (!fs.existsSync(logsFile)) {
299
- console.log('\n📝 No logs yet\n');
300
- return;
301
- }
302
-
303
- const logs = fs.readFileSync(logsFile, 'utf-8');
304
- const lines = logs.split('\n').slice(-50);
305
-
306
- console.log('\n📝 Recent logs (last 50 lines):\n');
307
- 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)));
308
369
  }
309
370
 
310
- // Reset configuration
311
371
  function reset() {
312
- const config = loadConfig();
313
-
314
- if (Object.keys(config).length === 0) {
315
- console.log('\n✅ No configuration to reset\n');
316
- return;
317
- }
318
-
319
372
  if (fs.existsSync(PROJECT_CONFIG)) {
320
373
  fs.unlinkSync(PROJECT_CONFIG);
321
- console.log(`✅ Removed project config: ${PROJECT_CONFIG}`);
374
+ logSuccess(`Removed project config: ${PROJECT_CONFIG}`);
322
375
  }
323
-
324
- console.log('✅ Configuration reset\n');
376
+ logSuccess("Configuration reset");
325
377
  }
326
378
 
327
- // Set GitHub token
328
- async function setGitHubToken() {
329
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
330
- const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
331
-
332
- try {
333
- console.log(`\n${colors.bgBlue(colors.bold(" 🔗 GitHub Integration "))}`);
334
- console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n`);
335
-
336
- console.log(`${colors.cyan("ℹ")} ${colors.dim("Generate a token at:")} ${colors.blue("https://github.com/settings/tokens")}`);
337
- console.log(`${colors.dim(" Required scope: 'repo' (Full control of private repositories)")}\n`);
338
-
339
- const token = await question(`${colors.cyan("GitHub Token")} › `);
340
-
341
- if (!token) {
342
- console.log(`\n${colors.red("✖")} Token is required\n`);
343
- rl.close();
344
- return;
345
- }
346
-
347
- const config = loadConfig();
348
- config.github_token = token;
349
- saveConfig(config, true);
350
-
351
- console.log(`\n${colors.green("✓")} ${colors.bold("GitHub token saved successfully!")}\n`);
352
- rl.close();
353
- } catch (error) {
354
- console.error(colors.red('Error saving token:'), error);
355
- rl.close();
356
- process.exit(1);
357
- }
358
- }
379
+ // ─── Issue Fixing ────────────────────────────────────────────────
359
380
 
360
- // List issues from a repository
361
381
  async function listIssues(repoArg, config) {
362
- // Parse repo from argument
363
382
  let owner, repo;
364
-
365
383
  if (repoArg.includes('github.com')) {
366
384
  const match = repoArg.match(/github\.com[/:]([^/]+)\/([^/\s]+)/);
367
- if (!match) {
368
- console.log(`\n${colors.red("✖")} Invalid GitHub URL`);
369
- return;
370
- }
371
- owner = match[1];
372
- repo = match[2].replace('.git', '');
385
+ if (!match) { logError("Invalid GitHub URL"); return; }
386
+ owner = match[1]; repo = match[2].replace('.git', '');
373
387
  } else if (repoArg.includes('/')) {
374
- const parts = repoArg.split('/');
375
- owner = parts[0];
376
- repo = parts[1];
377
- } else {
378
- console.log(`\n${colors.red("✖")} Invalid repository format`);
379
- return;
380
- }
388
+ [owner, repo] = repoArg.split('/');
389
+ } else { logError("Invalid repository format"); return; }
381
390
 
382
391
  const githubToken = config.github_token || process.env.GITHUB_TOKEN;
383
- if (!githubToken) {
384
- console.log(`\n${colors.red("✖")} GitHub token not found. Run ${colors.yellow("voria --set-github-token")}`);
385
- return;
386
- }
387
-
388
- 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; }
389
393
 
394
+ logInfo(`Fetching issues from ${colors.cyan(owner + "/" + repo)}...`);
390
395
  try {
391
- const response = await callPythonEngine({
392
- command: 'list_issues',
393
- owner: owner,
394
- repo: repo,
395
- github_token: githubToken,
396
- });
397
-
398
- if (response.status === 'error') {
399
- console.error(`${colors.red("✖")} Error: ${response.message}\n`);
400
- return;
401
- }
402
-
396
+ const response = await callPythonEngine({ command: 'list_issues', owner, repo, github_token: githubToken });
397
+ if (response.status === 'error') { logError(response.message); return; }
403
398
  const issues = response.data?.issues || [];
404
-
405
- if (issues.length === 0) {
406
- console.log(`${colors.yellow("📭")} No open issues found\n`);
407
- return;
408
- }
409
-
410
- console.log(`${colors.bgBlue(colors.bold(" 📋 Open Issues "))} ${colors.dim("in " + owner + "/" + repo + " (" + issues.length + " total)")}\n`);
411
-
399
+ if (issues.length === 0) { log(colors.dim("No open issues found")); return; }
400
+ logHeader(`Open Issues in ${owner}/${repo}`);
412
401
  for (const issue of issues) {
413
- const labels = issue.labels?.length > 0
414
- ? ` ${colors.dim("[")}${colors.yellow(issue.labels.join(", "))}${colors.dim("]")}`
415
- : '';
416
-
417
- console.log(`${colors.brightBlue("#" + issue.number)} ${colors.bold(issue.title)}${labels}`);
418
- console.log(` ${colors.dim("🔗")} ${colors.blue(issue.url)}`);
419
-
420
- if (issue.body) {
421
- const snippet = issue.body.length > 100 ? issue.body.substring(0, 100) + "..." : issue.body;
422
- console.log(` ${colors.dim("📝")} ${colors.dim(snippet.replace(/\n/g, " "))}`);
423
- }
424
- 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}`);
425
404
  }
426
-
427
- console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}`);
428
- console.log(`${colors.yellow("💡")} To fix an issue: ${colors.bold("voria issue " + owner + "/" + repo + " <number> --apply")}\n`);
429
- } catch (error) {
430
- console.error(`${colors.red("✖")} Error: ${error.message}\n`);
431
- }
405
+ logDivider();
406
+ logInfo(`To fix: ${colors.cyan("voria issue " + owner + "/" + repo + " <number> --apply")}`);
407
+ } catch (error) { logError(error.message); }
432
408
  }
433
409
 
434
- // Run plan command with LLM - Actually call Python engine
435
- // Enhanced plan command
436
410
  async function runPlanCommand(description, config, args) {
437
411
  console.log(BANNER);
438
- console.log(`${colors.bgBlue(colors.bold(" 📝 voria Plan "))} ${colors.blue("Architecting your fix")}`);
439
- console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}`);
440
- console.log(`${colors.cyan(description)}`);
441
- console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n`);
442
-
443
- console.log(`${colors.blue("⏳")} Analyzing codebase...`);
444
- 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...");
445
417
 
446
418
  try {
447
- let apiKey = config.api_key;
448
- if (!apiKey) {
449
- const envKey = `${(config.llm_provider || 'openai').toUpperCase()}_API_KEY`;
450
- apiKey = process.env[envKey];
451
- }
452
-
453
- if (!apiKey) {
454
- console.error(`${colors.red("✖")} Error: API key not found. Run ${colors.yellow("voria --config")}\n`);
455
- return;
456
- }
457
-
419
+ const apiKey = getApiKey(config);
458
420
  const response = await callPythonEngine({
459
- command: 'plan',
460
- description: description,
461
- repo_path: process.cwd(),
462
- provider: config.llm_provider || 'openai',
463
- api_key: apiKey,
464
- model: config.model,
421
+ command: 'plan', description, repo_path: process.cwd(),
422
+ provider: config.llm_provider || 'openai', api_key: apiKey, model: config.llm_model,
465
423
  });
466
-
467
- if (response.status === 'error') {
468
- console.error(`${colors.red("✖")} Error: ${response.message}\n`);
469
- 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));
470
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
+ }
471
434
 
472
- console.log(`${colors.green("✓")} ${colors.bold("Strategic Plan Generated!")}\n`);
473
-
474
- if (response.data && response.data.plan) {
475
- console.log(`${colors.bgBlue(colors.bold(" 📋 Project Blueprint "))}\n`);
476
- console.log(response.data.plan);
477
- console.log('\n');
478
- }
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}`);
479
458
 
480
- const isDryRun = args.includes('--dry-run');
481
- if (isDryRun) {
482
- 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}`); }
483
470
  } else {
484
- console.log(`${colors.green("")} Ready to implement!`);
485
- console.log(` Run: ${colors.bold("voria issue <repo> <number> --apply")}\n`);
471
+ logInfo(`Solution ready. Run with ${colors.bold("--apply")} to create PR.`);
486
472
  }
487
- } catch (error) {
488
- console.error(`${colors.red("✖")} Error: ${error.message}\n`);
489
- }
473
+ } catch (error) { logError(error.message); }
490
474
  }
491
475
 
492
- // Enhanced issue command
493
- async function runIssueCommand(repo, issueNumber, config, args) {
476
+ // ─── Tier 1: scan ────────────────────────────────────────────────
477
+
478
+ async function runScanCommand(config, args) {
494
479
  console.log(BANNER);
495
- console.log(`${colors.bgBlue(colors.bold(" 🚀 voria "))} ${colors.blue("Issue Fix Integration")}`);
496
- console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}`);
497
- console.log(`${colors.bold("🔗 Issue:")} ${colors.brightBlue("#" + issueNumber)} in ${colors.cyan(repo)}`);
498
- console.log(`${colors.bold("🤖 AI:")} ${colors.yellow(config.llm_provider || 'openai')}`);
499
- console.log(`${colors.bold("💰 Budget:")} ${colors.green("$" + (config.daily_budget || 10.0))}/day`);
500
- console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n`);
480
+ logHeader("voria Security Scan");
501
481
 
502
- console.log(`${colors.blue("⏳")} Fetching issue details...`);
503
- console.log(`${colors.blue("⏳")} Analyzing code context...`);
504
- console.log(`${colors.blue("⏳")} Generating optimized fix...\n`);
482
+ const categoryIdx = args.indexOf('--category');
483
+ const category = categoryIdx !== -1 && args[categoryIdx + 1] ? args[categoryIdx + 1] : 'all';
484
+
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...");
505
490
 
506
491
  try {
507
- let apiKey = config.api_key;
508
- if (!apiKey) {
509
- const envKey = `${(config.llm_provider || 'openai').toUpperCase()}_API_KEY`;
510
- 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
+ });
511
514
  }
512
-
513
- if (!apiKey) {
514
- console.error(`${colors.red("✖")} Error: API key not found. Run ${colors.yellow("voria --config")}\n`);
515
- return;
515
+ if (scan.recommendations?.length > 0) {
516
+ logHeader("Recommendations");
517
+ scan.recommendations.slice(0, 5).forEach(r => log(` ${colors.blue("")} ${r}`));
516
518
  }
519
+ logDivider();
520
+ logSuccess(`Scan complete: ${colors.blue(response.message)}`);
521
+ } catch (error) { logError(error.message); }
522
+ }
517
523
 
518
- const githubToken = config.github_token || process.env.GITHUB_TOKEN;
519
- if (!githubToken) {
520
- console.error(`${colors.red("✖")} Error: GitHub token not found. Run ${colors.yellow("voria --set-github-token")}\n`);
521
- return;
522
- }
524
+ // ─── Tier 1: test & list-tests ───────────────────────────────────
523
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();
530
+
531
+ try {
532
+ const apiKey = getApiKey(config);
524
533
  const response = await callPythonEngine({
525
- command: 'issue',
526
- issue_number: parseInt(issueNumber),
527
- repo: repo,
528
- repo_path: process.cwd(),
529
- provider: config.llm_provider || 'openai',
530
- api_key: apiKey,
531
- github_token: githubToken,
532
- 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(),
533
536
  });
534
-
535
- if (response.status === 'error') {
536
- console.error(`${colors.red("✖")} Error: ${response.message}\n`);
537
- 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
+ });
538
549
  }
539
-
540
- console.log(`${colors.green("")} ${colors.bold("Solution Generated!")}\n`);
541
-
542
- if (response.data) {
543
- console.log(`${colors.bold("📝 Title:")} ${response.data.issue_title}`);
550
+ if (result.recommendations?.length > 0) {
551
+ result.recommendations.forEach(r => log(` ${colors.blue("💡")} ${r}`));
544
552
  }
553
+ logDivider();
554
+ } catch (error) { logError(error.message); }
555
+ }
545
556
 
546
- const shouldApply = args.includes('--apply');
547
- if (shouldApply) {
548
- console.log(`\n${colors.blue("⏳")} Creating Pull Request...`);
549
- const prResponse = await callPythonEngine({
550
- command: 'create_pr',
551
- owner: repo.split('/')[0],
552
- repo: repo.split('/')[1],
553
- issue_number: parseInt(issueNumber),
554
- patch: response.data.patch,
555
- 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))}`);
556
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
+ }
557
579
 
558
- if (prResponse.status === 'success') {
559
- console.log(`\n${colors.green("🎉")} ${colors.bold("Pull Request Created!")}`);
560
- console.log(`${colors.bold("🔗 URL:")} ${colors.brightBlue(prResponse.data.pr_url)}`);
561
- console.log(`${colors.bold("🌿 Branch:")} ${colors.cyan(prResponse.data.branch)}\n`);
562
- } else {
563
- 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
+ }
564
601
  }
565
- } else {
566
- console.log(`\n${colors.yellow("💡")} Solution ready for review.`);
567
- console.log(` Run with ${colors.bold("--apply")} to automatically create a PR.\n`);
568
- }
569
- } catch (error) {
570
- console.error(`${colors.red("✖")} Error: ${error.message}\n`);
602
+ } catch {}
603
+ return snap;
571
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(() => {});
572
639
  }
573
640
 
574
- // Helper function to call Python engine
575
- async function callPythonEngine(command) {
576
- const { spawn } = await import('child_process');
577
- const pathMod = await import('path');
578
- const fsMod = await import('fs');
579
-
580
- return new Promise((resolve, reject) => {
581
- const pythonScript = pathMod.default.join(__dirname, '..', 'python', 'voria', 'engine.py');
582
- const pythonPath = pathMod.default.join(__dirname, '..', 'python');
583
-
584
- // Check if virtual environment exists
585
- const venvPath = pathMod.default.join(__dirname, '..', 'venv', 'bin', 'python');
586
- const pythonCmd = fsMod.default.existsSync(venvPath) ? venvPath : 'python3';
587
-
588
- // Add python directory to PYTHONPATH so voria module can be found
589
- const env = { ...process.env };
590
- const currentPath = env.PYTHONPATH || '';
591
- env.PYTHONPATH = pythonPath + (currentPath ? ':' + currentPath : '');
592
-
593
- const python = spawn(pythonCmd, [pythonScript], {
594
- cwd: process.cwd(),
595
- stdio: ['pipe', 'pipe', 'pipe'],
596
- env: env
597
- });
641
+ // ─── Tier 1: diff ────────────────────────────────────────────────
598
642
 
599
- let output = '';
600
- 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();
601
649
 
602
- python.stdout.on('data', (data) => {
603
- output += data.toString();
650
+ try {
651
+ const response = await callPythonEngine({
652
+ command: 'diff', ref_a: refA, ref_b: refB, repo_path: process.cwd(),
604
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; }
676
+
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;
605
681
 
606
- python.stderr.on('data', (data) => {
607
- errorOutput += data.toString();
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,
608
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
+ }
609
709
 
610
- python.on('close', (code) => {
611
- if (code !== 0) {
612
- console.debug(`Python engine exit code: ${code}`);
613
- console.debug(`Error output: ${errorOutput}`);
614
- }
615
-
616
- try {
617
- // Parse NDJSON responses
618
- const lines = output.trim().split('\n').filter(l => l.length > 0);
619
- const lastLine = lines[lines.length - 1];
620
- const response = JSON.parse(lastLine);
621
- resolve(response);
622
- } catch (e) {
623
- reject(new Error(`Failed to parse Python response: ${e.message}`));
624
- }
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(),
625
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
+ }
626
735
 
627
- // Send command to Python engine
628
- python.stdin.write(JSON.stringify(command) + '\n');
629
- python.stdin.end();
630
- });
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); }
631
774
  }
632
775
 
633
- // Helper function to call Rust core
776
+ // ─── Rust Core ───────────────────────────────────────────────────
777
+
634
778
  async function runRustCore(args) {
635
- const { spawn } = await import('child_process');
636
- const pathMod = await import('path');
637
- const fsMod = await import('fs');
638
-
639
779
  return new Promise((resolve, reject) => {
640
- // Try to find the rust binary
641
780
  const possiblePaths = [
642
- pathMod.default.join(__dirname, '..', 'target', 'release', 'voria'),
643
- pathMod.default.join(__dirname, '..', 'target', 'debug', 'voria'),
644
- pathMod.default.join(__dirname, '..', 'rust', 'target', 'release', 'voria'),
645
- 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'),
646
785
  ];
647
-
648
- let rustBin = possiblePaths.find(p => fsMod.default.existsSync(p));
649
-
786
+ let rustBin = possiblePaths.find(p => fs.existsSync(p));
650
787
  if (!rustBin) {
651
- // Check if it's in the system path
652
- try {
653
- execSync('voria --version', { stdio: 'ignore' });
654
- rustBin = 'voria';
655
- } catch (e) {
656
- return reject(new Error('Rust core binary (voria) not found. Please run "cargo build --release" in the rust directory.'));
657
- }
658
- }
659
-
788
+ try { execSync('voria --version', { stdio: 'ignore' }); rustBin = 'voria'; }
789
+ catch (e) { return reject(new Error('Rust core binary not found.')); }
790
+ }
660
791
  const child = spawn(rustBin, args, { stdio: 'inherit' });
661
- child.on('exit', (code) => {
662
- if (code === 0) resolve();
663
- else reject(new Error(`Rust core exited with code ${code}`));
664
- });
792
+ child.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`Exit code ${code}`)));
665
793
  });
666
794
  }
667
795
 
668
- // Main CLI router
796
+ // ─── Main CLI Router ─────────────────────────────────────────────
797
+
669
798
  async function main() {
670
799
  const args = process.argv.slice(2);
671
800
 
@@ -678,8 +807,7 @@ async function main() {
678
807
 
679
808
  switch (command) {
680
809
  case '--version':
681
- case '-v':
682
- console.log(`voria v${VERSION}`);
810
+ console.log(`${colors.blue("voria")} v${VERSION}`);
683
811
  break;
684
812
 
685
813
  case '--init':
@@ -696,14 +824,8 @@ async function main() {
696
824
 
697
825
  case '--list-issues': {
698
826
  const config = loadConfig();
699
- const repoArg = args[1];
700
- if (!repoArg) {
701
- console.log('\n❌ Please provide a repository');
702
- console.log(' Example: voria --list-issues owner/repo');
703
- console.log(' Or: voria --list-issues https://github.com/owner/repo\n');
704
- process.exit(1);
705
- }
706
- 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);
707
829
  break;
708
830
  }
709
831
 
@@ -720,102 +842,119 @@ async function main() {
720
842
  break;
721
843
 
722
844
  case 'plan': {
723
- const config = loadConfig();
724
- if (Object.keys(config).length === 0) {
725
- console.log('\n❌ voria not initialized');
726
- console.log(' Run: voria --init\n');
727
- process.exit(1);
728
- }
729
- const description = args.slice(1).join(' ');
730
- if (!description) {
731
- console.log('\n❌ Please provide a description');
732
- console.log(' Example: voria plan "Remove deprecated login() method"\n');
733
- process.exit(1);
734
- }
735
- 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);
736
849
  break;
737
850
  }
738
851
 
739
852
  case 'issue': {
740
- const config = loadConfig();
741
- if (Object.keys(config).length === 0) {
742
- console.log('\n❌ voria not initialized');
743
- console.log(' Run: voria --init\n');
744
- process.exit(1);
745
- }
746
-
747
- // Parse: voria issue owner/repo issue_number
748
- const issueArgs = args.slice(1);
749
- if (issueArgs.length < 2) {
750
- console.log('\n❌ Please provide repository and issue number');
751
- console.log(' Example: voria issue owner/repo 42\n');
752
- process.exit(1);
753
- }
754
-
755
- const repo = issueArgs[0];
756
- const issueNumber = issueArgs[1];
757
-
758
- 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);
759
856
  break;
760
857
  }
761
858
 
762
859
  case 'apply': {
763
- const config = loadConfig();
764
- if (Object.keys(config).length === 0) {
765
- console.log('\n❌ voria not initialized');
766
- console.log(' Run: voria --init\n');
767
- process.exit(1);
768
- }
860
+ requireInit();
769
861
  const patchFile = args[1];
770
- if (!patchFile) {
771
- console.log('\n❌ Please provide a patch file');
772
- console.log(' Example: voria apply fix.patch\n');
773
- process.exit(1);
774
- }
775
-
776
- if (!fs.existsSync(patchFile)) {
777
- console.log(`\n❌ Patch file not found: ${patchFile}\n`);
778
- 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>");
779
924
  }
925
+ break;
926
+ }
780
927
 
781
- const patch = fs.readFileSync(patchFile, 'utf-8');
782
- console.log(`\n🔧 Applying patch: ${patchFile}`);
783
-
928
+ case 'token': {
929
+ requireInit();
930
+ logHeader("Token Usage");
784
931
  try {
785
- const response = await callPythonEngine({
786
- command: 'apply',
787
- patch: patch,
788
- repo_path: process.cwd()
789
- });
790
-
791
- if (response.status === 'error') {
792
- console.error(`❌ Error: ${response.message}\n`);
793
- } else {
794
- console.log(`✅ ${response.message}\n`);
795
- }
796
- } catch (error) {
797
- console.error(`❌ Error: ${error.message}\n`);
798
- }
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); }
799
939
  break;
800
940
  }
801
941
 
802
942
  case '--graph':
803
- try {
804
- await runRustCore(['--graph']);
805
- } catch (error) {
806
- console.error(`${colors.red("")} Error running analysis: ${error.message}`);
807
- 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")}`);
808
947
  }
809
948
  break;
810
949
 
811
950
  default:
812
- console.log(`\n❌ Unknown command: ${command}`);
813
- console.log(' Run: voria --help\n');
951
+ logError(`Unknown command: ${colors.bold(command)}`);
952
+ logInfo(`Run ${colors.cyan("voria --help")} for usage.`);
814
953
  process.exit(1);
815
954
  }
816
955
  }
817
956
 
818
957
  main().catch(error => {
819
- console.error('Error:', error);
958
+ logError(error.message);
820
959
  process.exit(1);
821
960
  });