vanguard-cli 3.0.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 3.1.2 (2026-01-24)
4
+
5
+
6
+ ### Features
7
+
8
+ * implement scanner resilience and add test suite 🛡️🧪🧬✨ ([20310c3](https://github.com/bazobehram/vanguard/commit/20310c30d2787cbc2dedb0a19d8d2a5d2b308d0d))
9
+ * implement smart throttling and robust 429 recovery logic 🛡️❄️✨ ([4123794](https://github.com/bazobehram/vanguard/commit/4123794b1cced382fb628a1b828a466d83c24a3e))
10
+ * initial public release of Vanguard V3 Enterprise Edition ��️🚀✨ ([2335c1a](https://github.com/bazobehram/vanguard/commit/2335c1aa78a95a871cc600e62dd45431147088b9))
11
+
12
+ ## 3.1.1 (2026-01-24)
13
+
14
+
15
+ ### Features
16
+
17
+ * implement smart throttling and robust 429 recovery logic 🛡️❄️✨ ([4123794](https://github.com/bazobehram/vanguard/commit/4123794b1cced382fb628a1b828a466d83c24a3e))
18
+ * initial public release of Vanguard V3 Enterprise Edition ��️🚀✨ ([2335c1a](https://github.com/bazobehram/vanguard/commit/2335c1aa78a95a871cc600e62dd45431147088b9))
@@ -37,7 +37,14 @@ export async function handlePull(cmdOptions, programOptions) {
37
37
  }
38
38
 
39
39
  const scanner = new VanguardScanner(programOptions.model, context);
40
- const analysis = await scanner.scan('git_diff', diff);
40
+ let analysis;
41
+ try {
42
+ analysis = await scanner.scan('git_diff', diff, spinner);
43
+ } catch (err) {
44
+ spinner.fail(`AI Audit Failed: ${err.message}`);
45
+ console.log(chalk.yellow('\n💡 Tip: You might be rate-limited. Try again in 60s or run "vanguard config" to switch to local Ollama.'));
46
+ return;
47
+ }
41
48
  spinner.stop();
42
49
 
43
50
  showAlert(analysis);
@@ -107,14 +114,21 @@ export async function handleClone(url, directory, programOptions) {
107
114
  continue;
108
115
  }
109
116
 
110
- const result = await scanner.scan(file, content);
111
- if (result.verdict === 'BLOCK') {
112
- finalVerdict = 'BLOCK';
113
- allThreats.push(
114
- ...result.threats.map((t) => ({ ...t, file: path.relative(tempPath, file) }))
115
- );
116
- } else {
117
- CacheManager.save(file, content, 'SAFE');
117
+ try {
118
+ const result = await scanner.scan(file, content, scanSpinner);
119
+ if (result.verdict === 'BLOCK') {
120
+ finalVerdict = 'BLOCK';
121
+ allThreats.push(
122
+ ...result.threats.map((t) => ({ ...t, file: path.relative(tempPath, file) }))
123
+ );
124
+ } else {
125
+ CacheManager.save(file, content, 'SAFE');
126
+ }
127
+ } catch (err) {
128
+ scanSpinner.fail(`Audit Interrupted: ${err.message}`);
129
+ console.log(chalk.yellow('\n⚠️ Service Error. Part of the codebase remains unverified.'));
130
+ await cleanupSandbox(tempPath);
131
+ return;
118
132
  }
119
133
  }
120
134
 
@@ -10,7 +10,9 @@ import pLimit from 'p-limit';
10
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
11
  const THREATS_PATH = path.join(__dirname, '../threats.json');
12
12
 
13
- const limit = pLimit(3); // Enterprise concurrency limit: 3 files at a time
13
+ const limit = pLimit(1); // Conservative concurrency for free tier stability
14
+
15
+ export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
14
16
 
15
17
  export class VanguardScanner {
16
18
  constructor(modelOverride, intelligenceContext = '') {
@@ -19,36 +21,20 @@ export class VanguardScanner {
19
21
  this.intelligenceContext = intelligenceContext;
20
22
  }
21
23
 
22
- /**
23
- * Enterprise Filtering Logic: Skip binaries, images, and massive files.
24
- */
25
24
  shouldSkip(filePath, stats) {
26
25
  const ext = path.extname(filePath).toLowerCase();
27
26
  const binaryExtensions = [
28
- '.exe',
29
- '.dll',
30
- '.so',
31
- '.bin',
32
- '.png',
33
- '.jpg',
34
- '.jpeg',
35
- '.gif',
36
- '.pdf',
37
- '.zip',
38
- '.gz',
27
+ '.exe', '.dll', '.so', '.bin', '.png', '.jpg', '.jpeg', '.gif', '.pdf', '.zip', '.gz',
39
28
  ];
40
-
41
29
  if (binaryExtensions.includes(ext)) return true;
42
- if (stats.size > 100 * 1024) return true; // 100KB limit
30
+ if (stats.size > 100 * 1024) return true;
43
31
  if (filePath.includes('node_modules')) return true;
44
-
45
32
  return false;
46
33
  }
47
34
 
48
35
  async getSystemInstruction() {
49
36
  const threatsData = await fs.readFile(THREATS_PATH, 'utf-8');
50
37
  const threats = JSON.parse(threatsData);
51
-
52
38
  const categoriesListing = threats
53
39
  .map((t, i) => `${i + 1}. **${t.name}:** ${t.description}`)
54
40
  .join('\n');
@@ -62,44 +48,59 @@ ${categoriesListing}
62
48
 
63
49
  ${this.intelligenceContext}
64
50
 
65
- TASK: Combine the code analysis with the latest intelligence and vulnerability data above.
66
- If OSV reports a critical vulnerability or the remote rules flag a new pattern, YOU MUST VERDICT: BLOCK.
67
-
68
51
  JSON OUTPUT RULES:
69
- - risk_score: 0-100 (Be aggressive).
70
- - verdict: "BLOCK" (if risk found) or "SAFE".
71
- - threats: Array of objects: { "file": "filename", "line": number, "threat": "category", "reason": "why" }
72
- - summary: Clear high-level explanation for a developer.
52
+ - risk_score: 0-100.
53
+ - verdict: "BLOCK" or "SAFE".
54
+ - threats: Array of { "file", "line", "threat", "reason" }
55
+ - summary: Clear explanation.
73
56
 
74
57
  RESPONSE FORMAT (JSON ONLY):
75
- { "risk_score": 95, "verdict": "BLOCK", "threats": [{ "file": "lib/utils.js", "line": 42, "threat": "Secret Exfiltration", "reason": "Accesses .env and sends to remote server." }], "summary": "Critical vulnerability found." }
58
+ { "risk_score": 95, "verdict": "BLOCK", "threats": [], "summary": "..." }
76
59
  `;
77
60
  }
78
61
 
79
- async scan(filePath, content) {
80
- return limit(() => this.scanWithRetry(filePath, content));
62
+ async scan(filePath, content, spinner = null) {
63
+ // Random Jitter Throttling (1s - 3s)
64
+ if (this.provider === 'gemini') {
65
+ const jitter = Math.floor(Math.random() * 2000) + 1000;
66
+ await sleep(jitter);
67
+ }
68
+ return limit(() => this.scanWithRetry(filePath, content, spinner));
81
69
  }
82
70
 
83
- async scanWithRetry(filePath, content, retries = 3) {
84
- for (let i = 0; i < retries; i++) {
85
- try {
86
- if (this.provider === 'gemini') {
87
- return await this.scanWithGemini(content);
71
+ async scanWithRetry(filePath, content, spinner, attempt = 1) {
72
+ const maxAttempts = 3;
73
+ try {
74
+ if (this.provider === 'gemini') {
75
+ return await this.scanWithGemini(content);
76
+ } else {
77
+ return await this.scanWithOllama(content);
78
+ }
79
+ } catch (error) {
80
+ const isRateLimit = error.message.includes('429') || error.message.includes('Too Many Requests');
81
+ const isServerErr = error.message.includes('500') || error.message.includes('503');
82
+
83
+ if (isRateLimit && attempt <= maxAttempts) {
84
+ if (spinner) {
85
+ const originalText = spinner.text;
86
+ for (let i = 30; i > 0; i--) {
87
+ spinner.text = `❄️ Rate limit hit. Cooling down API (${i}s remaining)...`;
88
+ await sleep(1000);
89
+ }
90
+ spinner.text = originalText;
88
91
  } else {
89
- return await this.scanWithOllama(content);
90
- }
91
- } catch (error) {
92
- const isRateLimit =
93
- error.message.includes('429') || error.message.includes('Too Many Requests');
94
- if (isRateLimit && i < retries - 1) {
95
- const delay = Math.pow(2, i) * 1000;
96
- if (config.get('VERBOSE'))
97
- console.log(chalk.yellow(` ⚠ Rate limited. Retrying in ${delay}ms...`));
98
- await new Promise((res) => setTimeout(res, delay));
99
- continue;
92
+ console.log(chalk.yellow(`\n⚠️ Rate limit hit. Cooling down for 30s (Attempt ${attempt}/${maxAttempts})...`));
93
+ await sleep(30000);
100
94
  }
101
- throw error;
95
+ return this.scanWithRetry(filePath, content, spinner, attempt + 1);
102
96
  }
97
+
98
+ if (isServerErr && attempt === 1) {
99
+ if (spinner) spinner.text = '🔄 AI Server hiccup. Retrying immediately...';
100
+ return this.scanWithRetry(filePath, content, spinner, attempt + 1);
101
+ }
102
+
103
+ throw error;
103
104
  }
104
105
  }
105
106
 
@@ -108,13 +109,12 @@ RESPONSE FORMAT (JSON ONLY):
108
109
  if (!apiKey) throw new Error('Gemini API Key missing. Run "vanguard config"');
109
110
 
110
111
  const genAI = new GoogleGenerativeAI(apiKey);
111
- const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash-exp' });
112
+ // Switch to higher limit model
113
+ const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
112
114
  const instruction = await this.getSystemInstruction();
113
115
 
114
116
  const result = await model.generateContent({
115
- contents: [
116
- { role: 'user', parts: [{ text: `${instruction}\n\nFILE CONTENT:\n${content}` }] },
117
- ],
117
+ contents: [{ role: 'user', parts: [{ text: `${instruction}\n\nFILE CONTENT:\n${content}` }] }],
118
118
  });
119
119
 
120
120
  const response = await result.response;
package/package.json CHANGED
@@ -1,50 +1,51 @@
1
- {
2
- "name": "vanguard-cli",
3
- "version": "3.0.0",
4
- "description": "AI-Powered Supply Chain Firewall for Git",
5
- "type": "module",
6
- "bin": {
7
- "vanguard": "./bin/vanguard.js"
8
- },
9
- "scripts": {
10
- "test": "vitest run",
11
- "format": "prettier --write .",
12
- "lint": "eslint .",
13
- "release": "release-it",
14
- "release:dry": "release-it --dry-run"
15
- },
16
- "keywords": [],
17
- "author": "Behram Bazo",
18
- "license": "MIT",
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/bazob/vanguard.git"
22
- },
23
- "dependencies": {
24
- "@google/generative-ai": "^0.21.0",
25
- "boxen": "^8.0.1",
26
- "chalk": "^4.1.2",
27
- "cli-table3": "^0.6.5",
28
- "commander": "^13.1.0",
29
- "conf": "^12.0.0",
30
- "crypto-js": "^4.2.0",
31
- "figlet": "^1.8.0",
32
- "inquirer": "^8.2.4",
33
- "node-fetch": "^2.7.0",
34
- "ora": "^8.1.1",
35
- "p-limit": "^7.2.0",
36
- "simple-git": "^3.27.0"
37
- },
38
- "devDependencies": {
39
- "@eslint/js": "^9.39.2",
40
- "@release-it/conventional-changelog": "^10.0.4",
41
- "eslint": "^9.39.2",
42
- "eslint-config-prettier": "^10.1.8",
43
- "eslint-plugin-node": "^11.1.0",
44
- "globals": "^17.1.0",
45
- "nock": "^14.0.10",
46
- "prettier": "^3.8.1",
47
- "release-it": "^19.2.4",
48
- "vitest": "^4.0.18"
49
- }
50
- }
1
+ {
2
+ "name": "vanguard-cli",
3
+ "version": "3.1.2",
4
+ "description": "AI-Powered Supply Chain Firewall for Git",
5
+ "type": "module",
6
+ "bin": {
7
+ "vanguard": "./bin/vanguard.js"
8
+ },
9
+ "scripts": {
10
+ "test": "vitest run",
11
+ "format": "prettier --write .",
12
+ "lint": "eslint .",
13
+ "release": "release-it",
14
+ "release:dry": "release-it --dry-run",
15
+ "ship": "node scripts/ship.js"
16
+ },
17
+ "keywords": [],
18
+ "author": "Behram Bazo",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/bazob/vanguard.git"
23
+ },
24
+ "dependencies": {
25
+ "@google/generative-ai": "^0.21.0",
26
+ "boxen": "^8.0.1",
27
+ "chalk": "^4.1.2",
28
+ "cli-table3": "^0.6.5",
29
+ "commander": "^13.1.0",
30
+ "conf": "^12.0.0",
31
+ "crypto-js": "^4.2.0",
32
+ "figlet": "^1.8.0",
33
+ "inquirer": "^8.2.4",
34
+ "node-fetch": "^2.7.0",
35
+ "ora": "^8.1.1",
36
+ "p-limit": "^7.2.0",
37
+ "simple-git": "^3.27.0"
38
+ },
39
+ "devDependencies": {
40
+ "@eslint/js": "^9.39.2",
41
+ "@release-it/conventional-changelog": "^10.0.4",
42
+ "eslint": "^9.39.2",
43
+ "eslint-config-prettier": "^10.1.8",
44
+ "eslint-plugin-node": "^11.1.0",
45
+ "globals": "^17.1.0",
46
+ "nock": "^14.0.10",
47
+ "prettier": "^3.8.1",
48
+ "release-it": "^19.2.4",
49
+ "vitest": "^4.0.18"
50
+ }
51
+ }