vigthoria-cli 1.6.21 → 1.6.22

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.
@@ -15,6 +15,7 @@ interface ChatOptions {
15
15
  stream?: boolean;
16
16
  prompt?: string;
17
17
  json?: boolean;
18
+ bridge?: string;
18
19
  }
19
20
  export declare class ChatCommand {
20
21
  private config;
@@ -64,6 +65,8 @@ export declare class ChatCommand {
64
65
  private updateOperatorSpinner;
65
66
  constructor(config: Config, logger: Logger);
66
67
  run(options: ChatOptions): Promise<void>;
68
+ /** Handle an inbound admin command from the Commando Bridge. */
69
+ private handleAdminCommand;
67
70
  private ensureProjectWorkspace;
68
71
  private resolveProjectPath;
69
72
  private shouldUseManagedWorkspace;
@@ -46,6 +46,7 @@ const logger_js_1 = require("../utils/logger.js");
46
46
  const api_js_1 = require("../utils/api.js");
47
47
  const tools_js_1 = require("../utils/tools.js");
48
48
  const session_js_1 = require("../utils/session.js");
49
+ const bridge_client_js_1 = require("../utils/bridge-client.js");
49
50
  const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
50
51
  const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS || '3900000';
51
52
  const parsed = Number.parseInt(rawValue, 10);
@@ -395,6 +396,21 @@ class ChatCommand {
395
396
  this.directToolContinuationCount = 0;
396
397
  this.tools = new tools_js_1.AgenticTools(this.logger, this.currentProjectPath, async (action) => this.requestPermission(action), this.autoApprove);
397
398
  this.initializeSession(options.resume === true);
399
+ // ── Commando Bridge: connect if --bridge was specified ──────────
400
+ if (options.bridge) {
401
+ const bridgeClient = new bridge_client_js_1.BridgeClient({
402
+ bridgeUrl: options.bridge,
403
+ apiKey: this.config.get('authToken'),
404
+ onAdminCommand: (cmd) => this.handleAdminCommand(cmd),
405
+ });
406
+ await bridgeClient.connect();
407
+ const mode = this.operatorMode ? 'operator' : this.agentMode ? 'agent' : 'chat';
408
+ bridgeClient.emitStart({
409
+ command: mode,
410
+ flags: { model: this.currentModel, agent: this.agentMode, operator: this.operatorMode, autoApprove: this.autoApprove },
411
+ cwd: this.currentProjectPath,
412
+ });
413
+ }
398
414
  if (this.operatorMode && !this.hasOperatorAccess()) {
399
415
  if (this.currentSession) {
400
416
  this.currentSession.operatorMode = false;
@@ -406,9 +422,35 @@ class ChatCommand {
406
422
  }
407
423
  if (options.prompt) {
408
424
  await this.handleDirectPrompt(options.prompt);
425
+ (0, bridge_client_js_1.getBridgeClient)()?.emitEnd({ reason: 'prompt-complete' });
409
426
  return;
410
427
  }
411
428
  await this.startInteractiveChat();
429
+ (0, bridge_client_js_1.getBridgeClient)()?.emitEnd({ reason: 'interactive-exit' });
430
+ }
431
+ /** Handle an inbound admin command from the Commando Bridge. */
432
+ handleAdminCommand(cmd) {
433
+ switch (cmd.action) {
434
+ case 'ping':
435
+ (0, bridge_client_js_1.getBridgeClient)()?.emitModelResponse({ model: this.currentModel, chars: 0, hasToolCalls: false, preview: 'pong' });
436
+ break;
437
+ case 'set-model':
438
+ if (cmd.params?.value && typeof cmd.params.value === 'string') {
439
+ this.currentModel = cmd.params.value;
440
+ (0, bridge_client_js_1.getBridgeClient)()?.emitModeChange({ mode: this.operatorMode ? 'operator' : this.agentMode ? 'agent' : 'chat', model: this.currentModel });
441
+ if (!this.jsonOutput)
442
+ console.log(chalk_1.default.yellow(`[bridge] Model changed to: ${this.currentModel}`));
443
+ }
444
+ break;
445
+ case 'abort':
446
+ if (!this.jsonOutput)
447
+ console.log(chalk_1.default.red(`[bridge] Abort requested by admin`));
448
+ (0, bridge_client_js_1.getBridgeClient)()?.emitEnd({ reason: 'admin-abort' });
449
+ process.exit(0);
450
+ break;
451
+ default:
452
+ (0, bridge_client_js_1.getBridgeClient)()?.emitError({ message: `Unknown admin command: ${cmd.action}` });
453
+ }
412
454
  }
413
455
  ensureProjectWorkspace() {
414
456
  if (fs.existsSync(this.currentProjectPath)) {
@@ -635,12 +677,37 @@ class ChatCommand {
635
677
  this.logger.error(this.operatorAccessMessage());
636
678
  return;
637
679
  }
680
+ (0, bridge_client_js_1.getBridgeClient)()?.emitPrompt({ prompt, mode: 'operator', model: this.currentModel });
638
681
  const runtimeContext = await this.getPromptRuntimeContext(prompt);
639
682
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking like an operator...', spinner: 'clock' }).start();
640
- const executionPrompt = this.buildExecutionPrompt(prompt);
641
- const workflowType = this.isDiagnosticPrompt(prompt) || this.isAnalysisLookupPrompt(prompt)
683
+ const isLookup = this.isAnalysisLookupPrompt(prompt);
684
+ const workflowType = this.isDiagnosticPrompt(prompt) || isLookup
642
685
  ? 'analysis_only'
643
686
  : 'full_autonomy';
687
+ // For lightweight lookup tasks, prepend a grounding constraint so
688
+ // the operator returns a concise answer rather than a verbose report.
689
+ let executionPrompt = this.buildExecutionPrompt(prompt);
690
+ if (isLookup) {
691
+ // Inject workspace file listing into the prompt so the operator
692
+ // can answer "which file defines X" without a full agent loop.
693
+ let fileListing = '';
694
+ try {
695
+ const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
696
+ if (snapshot && snapshot.paths.length > 0) {
697
+ fileListing = '\n\nWorkspace file listing:\n' + snapshot.paths.slice(0, 80).join('\n');
698
+ }
699
+ }
700
+ catch { /* ignore */ }
701
+ executionPrompt = [
702
+ 'INSTRUCTION: This is a lightweight lookup task. Return ONLY the concise answer the user asked for.',
703
+ 'Do NOT produce a certification report, audit, or analysis blob.',
704
+ 'Do NOT return files_analyzed counts or summary statistics.',
705
+ 'Just answer the question directly.',
706
+ fileListing,
707
+ '',
708
+ executionPrompt,
709
+ ].join('\n');
710
+ }
644
711
  try {
645
712
  const response = await this.api.runOperatorWorkflow(executionPrompt, {
646
713
  workspacePath: this.currentProjectPath,
@@ -728,6 +795,7 @@ class ChatCommand {
728
795
  });
729
796
  }
730
797
  this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
798
+ (0, bridge_client_js_1.getBridgeClient)()?.emitPrompt({ prompt, mode: 'chat', model: this.currentModel });
731
799
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking...', spinner: 'clock' }).start();
732
800
  try {
733
801
  const response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
@@ -808,6 +876,7 @@ class ChatCommand {
808
876
  this.directToolContinuationCount = 0;
809
877
  this.agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
810
878
  this.tools.clearSessionApprovals();
879
+ (0, bridge_client_js_1.getBridgeClient)()?.emitPrompt({ prompt, mode: this.operatorMode ? 'operator' : 'agent', model: this.currentModel });
811
880
  this.ensureAgentSystemPrompt();
812
881
  this.messages.push({ role: 'user', content: this.buildScopedUserPrompt(prompt) });
813
882
  this.saveSession();
@@ -822,6 +891,12 @@ class ChatCommand {
822
891
  this.messages.push({ role: 'assistant', content: assistantMessage });
823
892
  const toolCalls = this.extractToolCalls(assistantMessage);
824
893
  const visibleText = this.stripToolPayloads(assistantMessage).trim();
894
+ (0, bridge_client_js_1.getBridgeClient)()?.emitModelResponse({
895
+ model: this.currentModel,
896
+ chars: assistantMessage.length,
897
+ hasToolCalls: toolCalls.length > 0,
898
+ preview: visibleText.slice(0, 300),
899
+ });
825
900
  if (toolCalls.length === 0) {
826
901
  // Phase 5: Quality gate — if the agent tries to conclude on the first
827
902
  // turn without any discovery, push it to gather evidence first.
@@ -1758,6 +1833,7 @@ class ChatCommand {
1758
1833
  if (!this.jsonOutput) {
1759
1834
  console.log(chalk_1.default.cyan(`⚙ Executing: ${call.tool}`));
1760
1835
  }
1836
+ (0, bridge_client_js_1.getBridgeClient)()?.emitToolCall({ tool: call.tool, args: call.args });
1761
1837
  let result = await this.tools.execute(call);
1762
1838
  // Phase 2: If a search tool failed (search_failed), retry with alternate approach
1763
1839
  const searchStatus = result.metadata?.searchStatus;
@@ -1779,6 +1855,7 @@ class ChatCommand {
1779
1855
  console.log(result.success ? chalk_1.default.gray(summary) : chalk_1.default.red(summary));
1780
1856
  }
1781
1857
  this.messages.push({ role: 'system', content: summary });
1858
+ (0, bridge_client_js_1.getBridgeClient)()?.emitToolResult({ tool: call.tool, success: result.success, preview: (result.output || result.error || '').slice(0, 300) });
1782
1859
  // Phase 5: Track tool evidence for quality gates
1783
1860
  const finalStatus = result.metadata?.searchStatus;
1784
1861
  if (finalStatus === 'search_failed') {
@@ -65,9 +65,16 @@ class EditCommand {
65
65
  const response = await this.api.chat([
66
66
  {
67
67
  role: 'system',
68
- content: `You are a code editor. Edit the provided code according to the user's instruction.
69
- Return ONLY the complete modified code without any explanation or markdown formatting.
70
- Preserve the original formatting and style unless the instruction specifically asks to change it.`,
68
+ content: `You are a precise code editor. Edit the provided code EXACTLY as the user instructs.
69
+
70
+ MANDATORY RULES:
71
+ 1. Return ONLY the complete modified file content. No explanation, no markdown.
72
+ 2. Preserve all original formatting, indentation, and style.
73
+ 3. Make ONLY the changes the instruction asks for — nothing more, nothing less.
74
+ 4. If the instruction says "Replace X with Y", then X must become Y verbatim.
75
+ 5. Do NOT add, remove, or rewrite lines the instruction didn't mention.
76
+ 6. Do NOT summarize, shorten, paraphrase, or "improve" the requested text.
77
+ 7. The instruction text is LITERAL — reproduce it character-for-character.`,
71
78
  },
72
79
  {
73
80
  role: 'user',
@@ -81,7 +88,7 @@ ${file.content}
81
88
 
82
89
  Instruction: ${instruction}
83
90
 
84
- Return the complete modified code:`,
91
+ Return the complete modified file content:`,
85
92
  },
86
93
  ], options.model);
87
94
  spinner.stop();
@@ -18,6 +18,11 @@ export declare class GenerateCommand {
18
18
  private fileUtils;
19
19
  constructor(config: Config, logger: Logger);
20
20
  run(description: string, options: GenerateOptions): Promise<void>;
21
+ /**
22
+ * Strip markdown code fences the model may wrap around the output.
23
+ * The CLI asks for raw code but some models still wrap in ```lang ... ```.
24
+ */
25
+ private stripMarkdownFences;
21
26
  private highlightCode;
22
27
  private highlightLine;
23
28
  private saveToFile;
@@ -81,13 +81,17 @@ class GenerateCommand {
81
81
  code = await this.api.generateCode(description, options.language, options.model);
82
82
  spinner.stop();
83
83
  }
84
- // Display generated code
85
- this.logger.section('Generated Code');
86
- console.log(chalk_1.default.gray('─'.repeat(60)));
87
- console.log(this.highlightCode(code, options.language));
88
- console.log(chalk_1.default.gray(''.repeat(60)));
89
- console.log();
90
- // Save options
84
+ // Strip any markdown fences the model may have wrapped around the code
85
+ code = this.stripMarkdownFences(code, options.language);
86
+ // Display generated code (skip display when --output is set to avoid noise)
87
+ if (!options.output) {
88
+ this.logger.section('Generated Code');
89
+ console.log(chalk_1.default.gray('─'.repeat(60)));
90
+ console.log(this.highlightCode(code, options.language));
91
+ console.log(chalk_1.default.gray('─'.repeat(60)));
92
+ console.log();
93
+ }
94
+ // Save options — when --output is specified, save directly (non-interactive)
91
95
  if (options.output) {
92
96
  await this.saveToFile(options.output, code);
93
97
  }
@@ -100,6 +104,25 @@ class GenerateCommand {
100
104
  this.logger.error('Generation failed:', error.message);
101
105
  }
102
106
  }
107
+ /**
108
+ * Strip markdown code fences the model may wrap around the output.
109
+ * The CLI asks for raw code but some models still wrap in ```lang ... ```.
110
+ */
111
+ stripMarkdownFences(code, language) {
112
+ const trimmed = code.trim();
113
+ // Try language-specific fence first
114
+ const langFenceRe = new RegExp(`^\`\`\`(?:${language})?\\s*\\n([\\s\\S]*?)\\n?\`\`\`\\s*$`, 'i');
115
+ const langMatch = trimmed.match(langFenceRe);
116
+ if (langMatch) {
117
+ return langMatch[1].trim();
118
+ }
119
+ // Try generic fence
120
+ const genericMatch = trimmed.match(/^```\w*\s*\n([\s\S]*?)\n?```\s*$/);
121
+ if (genericMatch) {
122
+ return genericMatch[1].trim();
123
+ }
124
+ return code;
125
+ }
103
126
  highlightCode(code, language) {
104
127
  // Basic syntax highlighting
105
128
  // In production, use a proper library like highlight.js
package/dist/index.js CHANGED
@@ -98,7 +98,7 @@ function getVersion() {
98
98
  catch (e) {
99
99
  // Fallback to hardcoded version
100
100
  }
101
- return '1.6.12';
101
+ return '1.6.22';
102
102
  }
103
103
  const VERSION = getVersion();
104
104
  /**
@@ -204,6 +204,7 @@ async function main() {
204
204
  .option('-w, --workflow <selector>', 'Route prompts through a named or explicit VigFlow workflow target')
205
205
  .option('--json', 'Emit machine-readable JSON output for direct prompt runs', false)
206
206
  .option('--auto-approve', 'Auto-approve agent actions (dangerous!)', false)
207
+ .option('--bridge <url>', 'Connect to Vigthoria Commando Bridge for remote admin observability')
207
208
  .action(async (options) => {
208
209
  const chat = new chat_js_1.ChatCommand(config, logger);
209
210
  await chat.run({
@@ -217,6 +218,7 @@ async function main() {
217
218
  autoApprove: options.autoApprove,
218
219
  resume: options.resume,
219
220
  prompt: options.prompt,
221
+ bridge: options.bridge,
220
222
  });
221
223
  });
222
224
  program
@@ -231,6 +233,7 @@ async function main() {
231
233
  .option('-w, --workflow <selector>', 'Route prompts through a named or explicit VigFlow workflow target')
232
234
  .option('--json', 'Emit machine-readable JSON output for direct prompt runs', false)
233
235
  .option('--auto-approve', 'Auto-approve agent actions (dangerous!)', false)
236
+ .option('--bridge <url>', 'Connect to Vigthoria Commando Bridge for remote admin observability')
234
237
  .action(async (options) => {
235
238
  const chat = new chat_js_1.ChatCommand(config, logger);
236
239
  await chat.run({
@@ -244,6 +247,7 @@ async function main() {
244
247
  autoApprove: options.autoApprove,
245
248
  resume: true,
246
249
  prompt: options.prompt,
250
+ bridge: options.bridge,
247
251
  });
248
252
  });
249
253
  // Agent command - Agentic mode (Vigthoria Autonomous)
@@ -259,6 +263,7 @@ async function main() {
259
263
  .option('-w, --workflow <selector>', 'Run the prompt through a named or explicit VigFlow workflow target')
260
264
  .option('--json', 'Emit machine-readable JSON output for direct prompt runs', false)
261
265
  .option('--auto-approve', 'Auto-approve all actions (dangerous!)', false)
266
+ .option('--bridge <url>', 'Connect to Vigthoria Commando Bridge for remote admin observability')
262
267
  .action(async (options) => {
263
268
  const chat = new chat_js_1.ChatCommand(config, logger);
264
269
  await chat.run({
@@ -272,6 +277,7 @@ async function main() {
272
277
  json: options.json,
273
278
  autoApprove: options.autoApprove,
274
279
  prompt: options.prompt,
280
+ bridge: options.bridge,
275
281
  });
276
282
  });
277
283
  program
@@ -285,6 +291,7 @@ async function main() {
285
291
  .option('-w, --workflow <selector>', 'Run the prompt through a named or explicit VigFlow workflow target')
286
292
  .option('--save-plan', 'Save the completed BMAD plan into VigFlow for rerun and audit', false)
287
293
  .option('--json', 'Emit machine-readable JSON output for direct prompt runs', false)
294
+ .option('--bridge <url>', 'Connect to Vigthoria Commando Bridge for remote admin observability')
288
295
  .action(async (options) => {
289
296
  const chat = new chat_js_1.ChatCommand(config, logger);
290
297
  await chat.run({
@@ -298,6 +305,7 @@ async function main() {
298
305
  savePlan: options.savePlan,
299
306
  json: options.json,
300
307
  prompt: options.prompt,
308
+ bridge: options.bridge,
301
309
  });
302
310
  });
303
311
  program
@@ -348,7 +348,7 @@ export declare class APIClient {
348
348
  }>;
349
349
  /**
350
350
  * Lightweight client-side heuristic scan: catches common code smells
351
- * so review never returns "score 30, no issues".
351
+ * AND logic/arithmetic bugs so review never returns "score 30, no issues".
352
352
  */
353
353
  private heuristicCodeIssues;
354
354
  fixCode(code: string, language: string, fixType: string): Promise<{
package/dist/utils/api.js CHANGED
@@ -307,13 +307,19 @@ class APIClient {
307
307
  }
308
308
  getVigFlowBaseUrls() {
309
309
  const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
310
+ // Put the remote gateway first, since local VigFlow servers are
311
+ // rarely running for end-user CLI installations. This avoids
312
+ // wasting connection-attempt time on 127.0.0.1 and hitting the
313
+ // remote gateway only after the local attempts have already
314
+ // errored — which surfaces as a confusing "last error" 404 in
315
+ // some setups.
310
316
  const urls = [
311
317
  process.env.VIGTHORIA_VIGFLOW_URL,
312
318
  process.env.VIGFLOW_URL,
313
319
  process.env.WORKFLOW_BUILDER_URL,
320
+ `${configuredApiUrl}/api/vigflow`,
314
321
  'http://127.0.0.1:5060',
315
322
  'http://127.0.0.1:5050',
316
- `${configuredApiUrl}/api/vigflow`,
317
323
  ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
318
324
  return [...new Set(urls)];
319
325
  }
@@ -2324,11 +2330,11 @@ document.addEventListener('DOMContentLoaded', () => {
2324
2330
  return result.message;
2325
2331
  }
2326
2332
  if (Array.isArray(data?.events)) {
2327
- const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string');
2333
+ const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string' && event.summary.trim());
2328
2334
  if (completionEvent) {
2329
2335
  return completionEvent.summary;
2330
2336
  }
2331
- const messageEvent = [...data.events].reverse().find((event) => event && event.type === 'message' && typeof event.content === 'string');
2337
+ const messageEvent = [...data.events].reverse().find((event) => event && event.type === 'message' && typeof event.content === 'string' && event.content.trim());
2332
2338
  if (messageEvent) {
2333
2339
  return messageEvent.content;
2334
2340
  }
@@ -2750,6 +2756,13 @@ document.addEventListener('DOMContentLoaded', () => {
2750
2756
  throw new Error(errors.join(' | '));
2751
2757
  }
2752
2758
  formatOperatorResponse(data = {}) {
2759
+ // If the server returned a direct answer field, prefer it (for lookup tasks)
2760
+ if (typeof data.answer === 'string' && data.answer.trim()) {
2761
+ return data.answer.trim();
2762
+ }
2763
+ if (typeof data.result === 'string' && data.result.trim()) {
2764
+ return data.result.trim();
2765
+ }
2753
2766
  const lines = [];
2754
2767
  if (data.summary) {
2755
2768
  lines.push(String(data.summary).trim());
@@ -3277,33 +3290,50 @@ document.addEventListener('DOMContentLoaded', () => {
3277
3290
  const response = await this.client.post('/api/ai/review', {
3278
3291
  code,
3279
3292
  language,
3280
- instructions: 'Return concrete, line-specific issues with severity. Every issue MUST reference a line number. If the score is below 50, you MUST list at least 2 specific issues.',
3293
+ instructions: [
3294
+ 'Return concrete, line-specific issues with severity.',
3295
+ 'Every issue MUST reference a line number.',
3296
+ 'If the score is below 50, you MUST list at least 2 specific issues.',
3297
+ 'Prioritize REAL BUGS over style issues:',
3298
+ '- Wrong arithmetic operators (+ instead of -, * instead of /, etc.)',
3299
+ '- Logic errors (function named "add" using subtraction, wrong comparisons)',
3300
+ '- Off-by-one errors, incorrect return values',
3301
+ '- Type mismatches, null/undefined access',
3302
+ 'Only report style issues (console.log, naming) AFTER listing all real bugs.',
3303
+ ].join(' '),
3281
3304
  });
3282
3305
  const raw = response.data ?? {};
3283
3306
  const score = typeof raw.score === 'number' ? raw.score : 0;
3284
3307
  const issues = Array.isArray(raw.issues) ? raw.issues : [];
3285
3308
  const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
3309
+ // Always run client-side heuristics and merge any findings the
3310
+ // server missed. This ensures arithmetic/logic bugs are surfaced
3311
+ // even when the server only reports style issues like console.log.
3312
+ const heuristic = this.heuristicCodeIssues(code, language);
3313
+ for (const h of heuristic) {
3314
+ // Avoid duplicating issues the server already reported on the same line
3315
+ const isDuplicate = issues.some((existing) => existing.line === h.line && existing.type === h.type);
3316
+ if (!isDuplicate) {
3317
+ issues.push(h);
3318
+ }
3319
+ }
3320
+ // Sort: errors first, then warnings, then info
3321
+ const severityOrder = { error: 0, warning: 1, info: 2 };
3322
+ issues.sort((a, b) => (severityOrder[a.severity] ?? 3) - (severityOrder[b.severity] ?? 3));
3286
3323
  // Prevent contradictory output: low score but zero issues.
3287
- // Run lightweight client-side heuristics to produce concrete findings.
3288
3324
  if (score < 50 && issues.length === 0) {
3289
- const heuristic = this.heuristicCodeIssues(code, language);
3290
- if (heuristic.length > 0) {
3291
- issues.push(...heuristic);
3292
- }
3293
- else {
3294
- issues.push({
3295
- type: 'quality',
3296
- line: 1,
3297
- message: `The analysis returned a low quality score (${score}/100) but did not enumerate specific issues. Re-run the review or inspect the file manually.`,
3298
- severity: 'warning',
3299
- });
3300
- }
3325
+ issues.push({
3326
+ type: 'quality',
3327
+ line: 1,
3328
+ message: `The analysis returned a low quality score (${score}/100) but did not enumerate specific issues. Re-run the review or inspect the file manually.`,
3329
+ severity: 'warning',
3330
+ });
3301
3331
  }
3302
3332
  return { score, issues, suggestions };
3303
3333
  }
3304
3334
  /**
3305
3335
  * Lightweight client-side heuristic scan: catches common code smells
3306
- * so review never returns "score 30, no issues".
3336
+ * AND logic/arithmetic bugs so review never returns "score 30, no issues".
3307
3337
  */
3308
3338
  heuristicCodeIssues(code, language) {
3309
3339
  const issues = [];
@@ -3327,8 +3357,38 @@ document.addEventListener('DOMContentLoaded', () => {
3327
3357
  if (line.length > 200) {
3328
3358
  issues.push({ type: 'style', line: lineNum, message: `Line exceeds 200 characters (${line.length}).`, severity: 'info' });
3329
3359
  }
3330
- // Limit to 10 heuristic issues
3331
- if (issues.length >= 10)
3360
+ // Arithmetic / logic bugs: function named 'add' but using subtraction
3361
+ if (/\breturn\s+\w+\s*-\s*\w+/.test(line)) {
3362
+ // Check if the enclosing function name implies addition
3363
+ for (let j = i; j >= Math.max(0, i - 5); j--) {
3364
+ if (/function\s+add\b/i.test(lines[j]) || /\badd\s*[=(]/.test(lines[j])) {
3365
+ issues.push({ type: 'logic', line: lineNum, message: 'Subtraction operator in a function named "add" — likely should be addition (+).', severity: 'error' });
3366
+ break;
3367
+ }
3368
+ }
3369
+ }
3370
+ // Multiplication where addition is expected
3371
+ if (/\breturn\s+\w+\s*\*\s*\w+/.test(line)) {
3372
+ for (let j = i; j >= Math.max(0, i - 5); j--) {
3373
+ if (/function\s+add\b/i.test(lines[j]) || /\badd\s*[=(]/.test(lines[j])) {
3374
+ issues.push({ type: 'logic', line: lineNum, message: 'Multiplication operator in a function named "add" — likely should be addition (+).', severity: 'error' });
3375
+ break;
3376
+ }
3377
+ }
3378
+ }
3379
+ // Division where subtraction/addition is expected in subtract
3380
+ if (/\breturn\s+\w+\s*\+\s*\w+/.test(line)) {
3381
+ for (let j = i; j >= Math.max(0, i - 5); j--) {
3382
+ if (/function\s+subtract\b/i.test(lines[j]) || /\bsubtract\s*[=(]/.test(lines[j])) {
3383
+ issues.push({ type: 'logic', line: lineNum, message: 'Addition operator in a function named "subtract" — likely should be subtraction (-).', severity: 'error' });
3384
+ break;
3385
+ }
3386
+ }
3387
+ }
3388
+ // Off-by-one: comparing with === where <= or >= may be needed
3389
+ // (not implemented yet — would require more complex AST analysis)
3390
+ // Limit to 15 heuristic issues
3391
+ if (issues.length >= 15)
3332
3392
  break;
3333
3393
  }
3334
3394
  return issues;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Vigthoria CLI → DevTools Bridge Telemetry Client
3
+ *
4
+ * Connects the local CLI to the remote bridge server in "commando" mode,
5
+ * streaming real-time activity (commands, tool calls, model responses,
6
+ * file edits, errors) and receiving admin-issued commands.
7
+ *
8
+ * Design principles:
9
+ * - Fire-and-forget: never blocks the CLI main flow
10
+ * - Auto-reconnects with exponential back-off
11
+ * - Opt-in via --bridge <url> flag
12
+ * - Sensitive data (API keys, tokens) is never transmitted
13
+ */
14
+ export type TelemetryEventType = 'cli:start' | 'cli:command' | 'cli:prompt' | 'cli:model-response' | 'cli:tool-call' | 'cli:tool-result' | 'cli:file-edit' | 'cli:error' | 'cli:end' | 'cli:mode-change' | 'cli:heartbeat';
15
+ export interface TelemetryEvent {
16
+ type: TelemetryEventType;
17
+ payload: Record<string, unknown>;
18
+ ts: number;
19
+ clientId: string;
20
+ }
21
+ export interface AdminCommand {
22
+ type: 'admin:command';
23
+ action: string;
24
+ params?: Record<string, unknown>;
25
+ requestId?: string;
26
+ }
27
+ export interface BridgeClientOptions {
28
+ bridgeUrl: string;
29
+ apiKey?: string;
30
+ machineLabel?: string;
31
+ onAdminCommand?: (cmd: AdminCommand) => void;
32
+ }
33
+ /** Get the active bridge client (may be null if --bridge was not used). */
34
+ export declare function getBridgeClient(): BridgeClient | null;
35
+ export declare class BridgeClient {
36
+ private ws;
37
+ private url;
38
+ private apiKey;
39
+ private machineLabel;
40
+ private clientId;
41
+ private connected;
42
+ private reconnectTimer;
43
+ private heartbeatTimer;
44
+ private queue;
45
+ private maxQueueSize;
46
+ private reconnectDelay;
47
+ private destroyed;
48
+ private onAdminCommand?;
49
+ constructor(opts: BridgeClientOptions);
50
+ connect(): Promise<void>;
51
+ destroy(): void;
52
+ get isConnected(): boolean;
53
+ /** CLI session started (command, flags, cwd). */
54
+ emitStart(data: {
55
+ command: string;
56
+ flags: Record<string, unknown>;
57
+ cwd: string;
58
+ }): void;
59
+ /** User entered a prompt / message. */
60
+ emitPrompt(data: {
61
+ prompt: string;
62
+ mode: string;
63
+ model: string;
64
+ }): void;
65
+ /** Model response received (summary only, not full content). */
66
+ emitModelResponse(data: {
67
+ model: string;
68
+ chars: number;
69
+ hasToolCalls: boolean;
70
+ preview: string;
71
+ }): void;
72
+ /** Tool is being called. */
73
+ emitToolCall(data: {
74
+ tool: string;
75
+ args: Record<string, string>;
76
+ }): void;
77
+ /** Tool finished executing. */
78
+ emitToolResult(data: {
79
+ tool: string;
80
+ success: boolean;
81
+ preview: string;
82
+ }): void;
83
+ /** File was written or edited. */
84
+ emitFileEdit(data: {
85
+ file: string;
86
+ action: 'write' | 'edit';
87
+ linesChanged: number;
88
+ }): void;
89
+ /** Error occurred. */
90
+ emitError(data: {
91
+ message: string;
92
+ code?: string;
93
+ }): void;
94
+ /** Mode changed (agent / operator / chat). */
95
+ emitModeChange(data: {
96
+ mode: string;
97
+ model: string;
98
+ }): void;
99
+ /** Session ended. */
100
+ emitEnd(data: {
101
+ reason: string;
102
+ }): void;
103
+ private emit;
104
+ private sendRaw;
105
+ private bufferEvent;
106
+ private flushQueue;
107
+ private scheduleReconnect;
108
+ private startHeartbeat;
109
+ private stopHeartbeat;
110
+ }
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+ /**
3
+ * Vigthoria CLI → DevTools Bridge Telemetry Client
4
+ *
5
+ * Connects the local CLI to the remote bridge server in "commando" mode,
6
+ * streaming real-time activity (commands, tool calls, model responses,
7
+ * file edits, errors) and receiving admin-issued commands.
8
+ *
9
+ * Design principles:
10
+ * - Fire-and-forget: never blocks the CLI main flow
11
+ * - Auto-reconnects with exponential back-off
12
+ * - Opt-in via --bridge <url> flag
13
+ * - Sensitive data (API keys, tokens) is never transmitted
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ var __importDefault = (this && this.__importDefault) || function (mod) {
49
+ return (mod && mod.__esModule) ? mod : { "default": mod };
50
+ };
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.BridgeClient = void 0;
53
+ exports.getBridgeClient = getBridgeClient;
54
+ const ws_1 = __importDefault(require("ws"));
55
+ const os = __importStar(require("os"));
56
+ // ── Singleton accessor ───────────────────────────────────────────────
57
+ let _instance = null;
58
+ /** Get the active bridge client (may be null if --bridge was not used). */
59
+ function getBridgeClient() {
60
+ return _instance;
61
+ }
62
+ // ── BridgeClient ─────────────────────────────────────────────────────
63
+ class BridgeClient {
64
+ ws = null;
65
+ url;
66
+ apiKey;
67
+ machineLabel;
68
+ clientId;
69
+ connected = false;
70
+ reconnectTimer = null;
71
+ heartbeatTimer = null;
72
+ queue = []; // buffered events while disconnected
73
+ maxQueueSize = 500;
74
+ reconnectDelay = 2000; // ms, doubles on failure up to 30 s
75
+ destroyed = false;
76
+ onAdminCommand;
77
+ constructor(opts) {
78
+ this.url = opts.bridgeUrl.replace(/\/$/, '');
79
+ if (!this.url.includes('/ws')) {
80
+ this.url = this.url.replace(/^http/, 'ws') + '/ws';
81
+ }
82
+ this.apiKey = opts.apiKey;
83
+ this.machineLabel = opts.machineLabel || os.hostname();
84
+ this.clientId = `cli-${this.machineLabel}-${Date.now().toString(36)}`;
85
+ this.onAdminCommand = opts.onAdminCommand;
86
+ _instance = this;
87
+ }
88
+ // ── Lifecycle ────────────────────────────────────────────────────
89
+ async connect() {
90
+ if (this.destroyed)
91
+ return;
92
+ return new Promise((resolve) => {
93
+ try {
94
+ this.ws = new ws_1.default(this.url, { handshakeTimeout: 8000 });
95
+ this.ws.on('open', () => {
96
+ this.connected = true;
97
+ this.reconnectDelay = 2000;
98
+ // Authenticate as CLI commando client
99
+ this.sendRaw({
100
+ type: 'auth',
101
+ payload: {
102
+ source: 'cli-commando',
103
+ clientId: this.clientId,
104
+ machineLabel: this.machineLabel,
105
+ hostname: os.hostname(),
106
+ platform: os.platform(),
107
+ arch: os.arch(),
108
+ nodeVersion: process.version,
109
+ auth: this.apiKey ? { apiKey: this.apiKey } : undefined,
110
+ },
111
+ });
112
+ // Flush buffered events
113
+ this.flushQueue();
114
+ this.startHeartbeat();
115
+ resolve();
116
+ });
117
+ this.ws.on('message', (raw) => {
118
+ try {
119
+ const msg = JSON.parse(raw.toString());
120
+ if (msg.type === 'admin:command' && this.onAdminCommand) {
121
+ this.onAdminCommand(msg);
122
+ }
123
+ }
124
+ catch {
125
+ // ignore unparseable messages
126
+ }
127
+ });
128
+ this.ws.on('close', () => {
129
+ this.connected = false;
130
+ this.stopHeartbeat();
131
+ this.scheduleReconnect();
132
+ });
133
+ this.ws.on('error', () => {
134
+ this.connected = false;
135
+ this.stopHeartbeat();
136
+ this.scheduleReconnect();
137
+ resolve(); // resolve even on failure – must never block CLI
138
+ });
139
+ }
140
+ catch {
141
+ resolve(); // swallow – bridge is optional
142
+ }
143
+ });
144
+ }
145
+ destroy() {
146
+ this.destroyed = true;
147
+ this.stopHeartbeat();
148
+ if (this.reconnectTimer)
149
+ clearTimeout(this.reconnectTimer);
150
+ if (this.ws) {
151
+ try {
152
+ this.ws.close();
153
+ }
154
+ catch { /* ignore */ }
155
+ }
156
+ this.ws = null;
157
+ this.connected = false;
158
+ if (_instance === this)
159
+ _instance = null;
160
+ }
161
+ get isConnected() {
162
+ return this.connected;
163
+ }
164
+ // ── Telemetry emitters (public API used by CLI code) ─────────────
165
+ /** CLI session started (command, flags, cwd). */
166
+ emitStart(data) {
167
+ this.emit('cli:start', data);
168
+ }
169
+ /** User entered a prompt / message. */
170
+ emitPrompt(data) {
171
+ this.emit('cli:prompt', data);
172
+ }
173
+ /** Model response received (summary only, not full content). */
174
+ emitModelResponse(data) {
175
+ this.emit('cli:model-response', data);
176
+ }
177
+ /** Tool is being called. */
178
+ emitToolCall(data) {
179
+ // Redact sensitive arg values
180
+ const safeArgs = {};
181
+ for (const [k, v] of Object.entries(data.args)) {
182
+ if (/key|token|password|secret|auth/i.test(k)) {
183
+ safeArgs[k] = '***';
184
+ }
185
+ else if (typeof v === 'string' && v.length > 2000) {
186
+ safeArgs[k] = v.slice(0, 200) + `...[${v.length} chars]`;
187
+ }
188
+ else {
189
+ safeArgs[k] = v;
190
+ }
191
+ }
192
+ this.emit('cli:tool-call', { tool: data.tool, args: safeArgs });
193
+ }
194
+ /** Tool finished executing. */
195
+ emitToolResult(data) {
196
+ this.emit('cli:tool-result', data);
197
+ }
198
+ /** File was written or edited. */
199
+ emitFileEdit(data) {
200
+ this.emit('cli:file-edit', data);
201
+ }
202
+ /** Error occurred. */
203
+ emitError(data) {
204
+ this.emit('cli:error', data);
205
+ }
206
+ /** Mode changed (agent / operator / chat). */
207
+ emitModeChange(data) {
208
+ this.emit('cli:mode-change', data);
209
+ }
210
+ /** Session ended. */
211
+ emitEnd(data) {
212
+ this.emit('cli:end', data);
213
+ }
214
+ // ── Internals ────────────────────────────────────────────────────
215
+ emit(type, payload) {
216
+ const event = {
217
+ type,
218
+ payload,
219
+ ts: Date.now(),
220
+ clientId: this.clientId,
221
+ };
222
+ const json = JSON.stringify(event);
223
+ if (this.connected && this.ws?.readyState === ws_1.default.OPEN) {
224
+ try {
225
+ this.ws.send(json);
226
+ }
227
+ catch {
228
+ this.bufferEvent(json);
229
+ }
230
+ }
231
+ else {
232
+ this.bufferEvent(json);
233
+ }
234
+ }
235
+ sendRaw(obj) {
236
+ if (this.ws?.readyState === ws_1.default.OPEN) {
237
+ this.ws.send(JSON.stringify(obj));
238
+ }
239
+ }
240
+ bufferEvent(json) {
241
+ if (this.queue.length >= this.maxQueueSize) {
242
+ this.queue.shift(); // drop oldest
243
+ }
244
+ this.queue.push(json);
245
+ }
246
+ flushQueue() {
247
+ while (this.queue.length > 0 && this.ws?.readyState === ws_1.default.OPEN) {
248
+ const msg = this.queue.shift();
249
+ try {
250
+ this.ws.send(msg);
251
+ }
252
+ catch {
253
+ break;
254
+ }
255
+ }
256
+ }
257
+ scheduleReconnect() {
258
+ if (this.destroyed || this.reconnectTimer)
259
+ return;
260
+ this.reconnectTimer = setTimeout(() => {
261
+ this.reconnectTimer = null;
262
+ this.connect();
263
+ }, this.reconnectDelay);
264
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
265
+ }
266
+ startHeartbeat() {
267
+ this.heartbeatTimer = setInterval(() => {
268
+ this.emit('cli:heartbeat', { uptime: process.uptime() });
269
+ }, 30000);
270
+ }
271
+ stopHeartbeat() {
272
+ if (this.heartbeatTimer) {
273
+ clearInterval(this.heartbeatTimer);
274
+ this.heartbeatTimer = null;
275
+ }
276
+ }
277
+ }
278
+ exports.BridgeClient = BridgeClient;
@@ -356,6 +356,10 @@ class AgenticTools {
356
356
  * Execute a tool call with enhanced error handling and retry logic
357
357
  */
358
358
  async execute(call) {
359
+ // Guard against malformed tool calls with undefined or empty tool names
360
+ if (!call.tool || typeof call.tool !== 'string' || !call.tool.trim()) {
361
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid tool call: tool name is ${call.tool === undefined ? 'undefined' : 'empty'}`, `Provide a valid tool name. Available tools: ${AgenticTools.getToolDefinitions().map(t => t.name).join(', ')}`);
362
+ }
359
363
  const normalizedCall = this.normalizeToolCall(call);
360
364
  const tool = AgenticTools.getToolDefinitions().find(t => t.name === normalizedCall.tool);
361
365
  if (!tool) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.21",
3
+ "version": "1.6.22",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -21,6 +21,7 @@
21
21
  "dev": "ts-node src/index.ts",
22
22
  "test": "npm run test:cli",
23
23
  "test:cli": "npm run build && node scripts/test-cli-suite.js",
24
+ "test:regression": "npm run build && node scripts/test-regression-1.6.22.js",
24
25
  "test:agent:smoke": "npm run build && node scripts/test-agent-smoke.js",
25
26
  "test:agent:routing": "npm run build && node scripts/test-agent-routing-policy.js",
26
27
  "test:agent:context": "npm run build && node scripts/test-agent-context-trace-e2e.js",