vibehacker 4.1.0

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.
@@ -0,0 +1,387 @@
1
+ 'use strict';
2
+
3
+ // ── Provider Definitions ──────────────────────────────────────────────────────
4
+
5
+ const VH_GATEWAY_URL = process.env.VH_GATEWAY_URL || 'https://api.vibsecurity.com/v1';
6
+
7
+ const PROVIDERS = {
8
+ vibehacker: {
9
+ type: 'vibehacker',
10
+ name: 'Vibe Hacker',
11
+ shortName: 'Vibe Hacker',
12
+ baseURL: VH_GATEWAY_URL,
13
+ freeNote: '100 req/day free',
14
+ tier: 'free',
15
+ detectKey: k => k.startsWith('vh_'),
16
+ getKey: () => 'Sign up at https://vibsecurity.com',
17
+ models: [
18
+ { id: 'meta-llama/llama-3.3-70b-instruct:free', name: 'Vibe Model', contextWindow: 131072, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
19
+ { id: 'mistralai/mistral-small-3.1-24b-instruct:free',name: 'Vibe Model', contextWindow: 131072, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
20
+ { id: 'google/gemma-3-27b-it:free', name: 'Vibe Model', contextWindow: 131072, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
21
+ { id: 'qwen/qwen3-235b-a22b:free', name: 'Vibe Model', contextWindow: 32768, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
22
+ { id: 'qwen/qwen3-coder-480b-a35b-instruct:free', name: 'Vibe Model', contextWindow: 32768, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
23
+ { id: 'deepseek/deepseek-r1:free', name: 'Vibe Model', contextWindow: 163840, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
24
+ ],
25
+ },
26
+
27
+ openrouter: {
28
+ type: 'openrouter',
29
+ name: 'Vibe Hacker',
30
+ shortName: 'Vibe Hacker',
31
+ baseURL: 'https://openrouter.ai/api/v1',
32
+ freeNote: '100 req/day free',
33
+ tier: 'free',
34
+ detectKey: k => k.startsWith('sk-or-v1-'),
35
+ getKey: () => 'vibsecurity.com/login',
36
+ models: [
37
+ { id: 'meta-llama/llama-3.3-70b-instruct:free', name: 'Vibe Model', contextWindow: 131072, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
38
+ { id: 'mistralai/mistral-small-3.1-24b-instruct:free',name: 'Vibe Model', contextWindow: 131072, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
39
+ { id: 'google/gemma-3-27b-it:free', name: 'Vibe Model', contextWindow: 131072, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
40
+ { id: 'qwen/qwen3-235b-a22b:free', name: 'Vibe Model', contextWindow: 32768, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
41
+ { id: 'qwen/qwen3-coder-480b-a35b-instruct:free', name: 'Vibe Model', contextWindow: 32768, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
42
+ { id: 'nvidia/llama-3.1-nemotron-ultra-253b-v1:free', name: 'Vibe Model', contextWindow: 131072, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
43
+ { id: 'deepseek/deepseek-r1:free', name: 'Vibe Model', contextWindow: 163840, maxTokens: 8192, provider: 'Vibe Hacker', free: true },
44
+ ],
45
+ },
46
+
47
+ groq: {
48
+ type: 'groq',
49
+ name: 'Groq',
50
+ shortName: 'Groq',
51
+ baseURL: 'https://api.groq.com/openai/v1',
52
+ freeNote: '14,400 req/day free',
53
+ tier: 'free',
54
+ detectKey: k => k.startsWith('gsk_'),
55
+ getKey: () => 'console.groq.com → API Keys → Create Key',
56
+ models: [
57
+ { id: 'llama-3.1-8b-instant', name: 'Llama 3.1 8B', contextWindow: 131072, maxTokens: 8192, provider: 'Groq', free: true },
58
+ { id: 'llama-3.3-70b-versatile', name: 'Llama 3.3 70B', contextWindow: 131072, maxTokens: 8192, provider: 'Groq', free: true },
59
+ { id: 'llama-4-scout-17b-16e-instruct', name: 'Llama 4 Scout 17B', contextWindow: 131072, maxTokens: 8192, provider: 'Groq', free: true },
60
+ { id: 'moonshotai/kimi-k2-instruct', name: 'Kimi K2', contextWindow: 131072, maxTokens: 8192, provider: 'Groq', free: true },
61
+ { id: 'qwen/qwen3-32b', name: 'Qwen3 32B', contextWindow: 32768, maxTokens: 8192, provider: 'Groq', free: true },
62
+ ],
63
+ },
64
+
65
+ cerebras: {
66
+ type: 'cerebras',
67
+ name: 'Cerebras',
68
+ shortName: 'Cbrs',
69
+ baseURL: 'https://api.cerebras.ai/v1',
70
+ freeNote: '1M tokens/day free',
71
+ tier: 'free',
72
+ detectKey: k => k.startsWith('csk-'),
73
+ getKey: () => 'inference.cerebras.ai → API Keys → Create',
74
+ models: [
75
+ { id: 'qwen-3-235b-a22b', name: 'Qwen3 235B', contextWindow: 32768, maxTokens: 8192, provider: 'Cerebras', free: true },
76
+ { id: 'llama3.1-8b', name: 'Llama 3.1 8B', contextWindow: 131072, maxTokens: 8192, provider: 'Cerebras', free: true },
77
+ ],
78
+ },
79
+
80
+ gemini: {
81
+ type: 'gemini',
82
+ name: 'Gemini',
83
+ shortName: 'Gem',
84
+ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
85
+ freeNote: '1000 req/day free',
86
+ tier: 'free',
87
+ detectKey: k => k.startsWith('AIza'),
88
+ getKey: () => 'aistudio.google.com → Get API key → Create',
89
+ models: [
90
+ { id: 'gemini-2.5-flash-lite', name: 'Gemini 2.5 Flash Lite', contextWindow: 1000000, maxTokens: 8192, provider: 'Google', free: true },
91
+ { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', contextWindow: 1000000, maxTokens: 8192, provider: 'Google', free: true },
92
+ { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', contextWindow: 1000000, maxTokens: 8192, provider: 'Google', free: true },
93
+ ],
94
+ },
95
+
96
+ mistral: {
97
+ type: 'mistral',
98
+ name: 'Mistral',
99
+ shortName: 'Mist',
100
+ baseURL: 'https://api.mistral.ai/v1',
101
+ freeNote: '~1B tokens/month free',
102
+ tier: 'free',
103
+ detectKey: k => k.startsWith('mistral:'),
104
+ getKey: () => 'console.mistral.ai → API Keys → then: /addkey mistral:<key>',
105
+ models: [
106
+ { id: 'codestral-latest', name: 'Codestral', contextWindow: 256000, maxTokens: 8192, provider: 'Mistral', free: true },
107
+ { id: 'mistral-large-latest', name: 'Mistral Large', contextWindow: 131072, maxTokens: 8192, provider: 'Mistral', free: true },
108
+ ],
109
+ },
110
+
111
+ anthropic: {
112
+ type: 'anthropic',
113
+ name: 'Anthropic',
114
+ shortName: 'Ant',
115
+ baseURL: 'https://api.anthropic.com/v1',
116
+ freeNote: 'Pay-per-use',
117
+ tier: 'paid',
118
+ detectKey: k => k.startsWith('sk-ant-'),
119
+ getKey: () => 'console.anthropic.com → API Keys → Create Key',
120
+ models: [
121
+ { id: 'claude-opus-4-5', name: 'Claude Opus 4.5', contextWindow: 200000, maxTokens: 8192, provider: 'Anthropic', free: false },
122
+ { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5', contextWindow: 200000, maxTokens: 8192, provider: 'Anthropic', free: false },
123
+ { id: 'claude-haiku-3-5', name: 'Claude Haiku 3.5', contextWindow: 200000, maxTokens: 8192, provider: 'Anthropic', free: false },
124
+ ],
125
+ },
126
+
127
+ openai: {
128
+ type: 'openai',
129
+ name: 'OpenAI',
130
+ shortName: 'OAI',
131
+ baseURL: 'https://api.openai.com/v1',
132
+ freeNote: 'Pay-per-use',
133
+ tier: 'paid',
134
+ detectKey: k => k.startsWith('sk-') && !k.startsWith('sk-or-v1-') && !k.startsWith('sk-ant-'),
135
+ getKey: () => 'platform.openai.com → API Keys → Create Key',
136
+ models: [
137
+ { id: 'gpt-4.1', name: 'GPT-4.1', contextWindow: 128000, maxTokens: 8192, provider: 'OpenAI', free: false },
138
+ { id: 'gpt-4o', name: 'GPT-4o', contextWindow: 128000, maxTokens: 8192, provider: 'OpenAI', free: false },
139
+ { id: 'gpt-4o-mini', name: 'GPT-4o Mini', contextWindow: 128000, maxTokens: 8192, provider: 'OpenAI', free: false },
140
+ { id: 'o4-mini', name: 'o4-mini', contextWindow: 128000, maxTokens: 8192, provider: 'OpenAI', free: false },
141
+ ],
142
+ },
143
+
144
+ deepseek: {
145
+ type: 'deepseek',
146
+ name: 'DeepSeek',
147
+ shortName: 'DS',
148
+ baseURL: 'https://api.deepseek.com/v1',
149
+ freeNote: 'Pay-per-use (cheap)',
150
+ tier: 'paid',
151
+ detectKey: k => k.startsWith('sk-ds-') || k.startsWith('dsk-'),
152
+ getKey: () => 'platform.deepseek.com → API Keys → Create',
153
+ models: [
154
+ { id: 'deepseek-chat', name: 'DeepSeek V3', contextWindow: 131072, maxTokens: 8192, provider: 'DeepSeek', free: false },
155
+ { id: 'deepseek-reasoner', name: 'DeepSeek R1', contextWindow: 131072, maxTokens: 8192, provider: 'DeepSeek', free: false },
156
+ ],
157
+ },
158
+
159
+ xai: {
160
+ type: 'xai',
161
+ name: 'xAI (Grok)',
162
+ shortName: 'xAI',
163
+ baseURL: 'https://api.x.ai/v1',
164
+ freeNote: '$25/mo free credits',
165
+ tier: 'free',
166
+ detectKey: k => k.startsWith('xai-'),
167
+ getKey: () => 'console.x.ai → API Keys → Create Key',
168
+ models: [
169
+ { id: 'grok-3', name: 'Grok 3', contextWindow: 131072, maxTokens: 8192, provider: 'xAI', free: true },
170
+ { id: 'grok-3-mini', name: 'Grok 3 Mini', contextWindow: 131072, maxTokens: 8192, provider: 'xAI', free: true },
171
+ ],
172
+ },
173
+
174
+ together: {
175
+ type: 'together',
176
+ name: 'Together',
177
+ shortName: 'Togth',
178
+ baseURL: 'https://api.together.xyz/v1',
179
+ freeNote: '$1 free credit',
180
+ tier: 'free',
181
+ detectKey: k => k.startsWith('tog-') || k.startsWith('together-'),
182
+ getKey: () => 'api.together.xyz → Settings → API Keys',
183
+ models: [
184
+ { id: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', name: 'Llama 3.3 70B', contextWindow: 131072, maxTokens: 8192, provider: 'Together', free: true },
185
+ { id: 'Qwen/Qwen2.5-72B-Instruct-Turbo', name: 'Qwen 2.5 72B', contextWindow: 131072, maxTokens: 8192, provider: 'Together', free: true },
186
+ { id: 'deepseek-ai/DeepSeek-R1', name: 'DeepSeek R1', contextWindow: 131072, maxTokens: 8192, provider: 'Together', free: true },
187
+ ],
188
+ },
189
+ };
190
+
191
+ // ── ProviderManager ───────────────────────────────────────────────────────────
192
+
193
+ class ProviderManager {
194
+ constructor(modelManager) {
195
+ this._mm = modelManager;
196
+ this._list = [];
197
+ this._idx = 0;
198
+ this._modelIdx = {};
199
+ this._limited = new Set();
200
+ this._load();
201
+ }
202
+
203
+ _load() {
204
+ const config = require('./config');
205
+ const saved = config.providers || [];
206
+
207
+ // Detect primary type from stored key
208
+ const primaryType = config.apiKey && config.apiKey.startsWith('vh_')
209
+ ? 'vibehacker'
210
+ : 'openrouter';
211
+
212
+ this._list = [{ type: primaryType, apiKey: config.apiKey }];
213
+ for (const p of saved) {
214
+ if (p.type && p.apiKey && p.type !== primaryType) {
215
+ this._list.push({ type: p.type, apiKey: p.apiKey });
216
+ }
217
+ }
218
+ this._applyCurrent();
219
+ }
220
+
221
+ _save() {
222
+ const toSave = this._list
223
+ .filter(p => p.type !== 'vibehacker' && p.type !== 'openrouter')
224
+ .map(p => ({ type: p.type, apiKey: p.apiKey }));
225
+ const fs = require('fs');
226
+ const path = require('path');
227
+ const os = require('os');
228
+ const dir = path.join(os.homedir(), '.vibehacker');
229
+ const file = path.join(dir, 'config.json');
230
+ try {
231
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
232
+ let ex = {};
233
+ try { ex = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) {}
234
+ fs.writeFileSync(file, JSON.stringify({ ...ex, providers: toSave }, null, 2));
235
+ } catch (_) {}
236
+ }
237
+
238
+ _applyCurrent() {
239
+ const config = require('./config');
240
+ const p = this._list[this._idx] || this._list[0];
241
+ if (!p) return;
242
+ const def = PROVIDERS[p.type];
243
+ if (!def) return;
244
+ config.apiKey = p.apiKey;
245
+ config.baseURL = def.baseURL;
246
+ }
247
+
248
+ current() {
249
+ const p = this._list[this._idx] || this._list[0];
250
+ const def = PROVIDERS[p.type] || PROVIDERS.openrouter;
251
+ return { ...def, apiKey: p.apiKey };
252
+ }
253
+
254
+ currentModel() {
255
+ const p = this.current();
256
+ if (p.type === 'openrouter') return this._mm.current();
257
+ const models = p.models || [];
258
+ const idx = Math.min(this._modelIdx[p.type] || 0, models.length - 1);
259
+ return models[Math.max(0, idx)];
260
+ }
261
+
262
+ currentModels() {
263
+ const p = this.current();
264
+ if (p.type === 'openrouter') return this._mm.list();
265
+ return p.models || [];
266
+ }
267
+
268
+ list() {
269
+ return this._list.map((p, i) => ({
270
+ ...(PROVIDERS[p.type] || {}),
271
+ apiKey: p.apiKey,
272
+ active: i === this._idx,
273
+ limited: this._limited.has(p.type),
274
+ index: i,
275
+ }));
276
+ }
277
+
278
+ currentIdx() { return this._idx; }
279
+ allLimited() { return this._list.every(p => this._limited.has(p.type)); }
280
+ isCurrentFree() {
281
+ const m = this.currentModel();
282
+ if (m && typeof m.free === 'boolean') return m.free;
283
+ return (PROVIDERS[this.current().type] || {}).tier === 'free';
284
+ }
285
+
286
+ selectModelById(id) {
287
+ const p = this.current();
288
+ if (p.type === 'openrouter') { this._mm.selectById(id); return; }
289
+ const idx = (p.models || []).findIndex(m => m.id === id);
290
+ if (idx >= 0) this._modelIdx[p.type] = idx;
291
+ }
292
+
293
+ selectByType(type) {
294
+ const idx = this._list.findIndex(p => p.type === type);
295
+ if (idx >= 0) { this._idx = idx; this._applyCurrent(); }
296
+ }
297
+
298
+ // Silent model rotation on 429 — pick next model in current provider
299
+ rotateModel() {
300
+ const p = this.current();
301
+ const models = p.type === 'openrouter' ? this._mm.list() : (p.models || []);
302
+ const curIdx = this._modelIdx[p.type] || 0;
303
+ const nextIdx = (curIdx + 1) % models.length;
304
+ this._modelIdx[p.type] = nextIdx;
305
+ if (p.type === 'openrouter') {
306
+ this._mm.selectById(models[nextIdx].id);
307
+ }
308
+ return models[nextIdx];
309
+ }
310
+
311
+ add(apiKey) {
312
+ let strippedKey = apiKey;
313
+ const colonIdx = apiKey.indexOf(':');
314
+ if (colonIdx > 0 && colonIdx < 12) {
315
+ const prefix = apiKey.substring(0, colonIdx).toLowerCase();
316
+ if (PROVIDERS[prefix]) {
317
+ strippedKey = apiKey.substring(colonIdx + 1).trim();
318
+ apiKey = prefix + ':' + strippedKey;
319
+ }
320
+ }
321
+ const type = this._detectType(apiKey);
322
+ const actualKey = type === 'mistral' ? strippedKey : apiKey;
323
+
324
+ if (type === 'vibehacker' || type === 'openrouter') {
325
+ const config = require('./config');
326
+ config.setApiKey(actualKey);
327
+ this._list = this._list.filter(p => p.type !== 'vibehacker' && p.type !== 'openrouter');
328
+ this._list.unshift({ type, apiKey: actualKey });
329
+ this._idx = 0;
330
+ this._applyCurrent();
331
+ } else {
332
+ const existing = this._list.findIndex(p => p.type === type);
333
+ if (existing >= 0) this._list[existing].apiKey = strippedKey || actualKey;
334
+ else this._list.push({ type, apiKey: strippedKey || actualKey });
335
+ }
336
+
337
+ this._limited.delete(type);
338
+ this._save();
339
+ return type;
340
+ }
341
+
342
+ remove(type) {
343
+ if (type === 'openrouter') return;
344
+ this._list = this._list.filter(p => p.type !== type);
345
+ if (this._idx >= this._list.length) this._idx = 0;
346
+ this._limited.delete(type);
347
+ this._save();
348
+ this._applyCurrent();
349
+ }
350
+
351
+ markRateLimited() {
352
+ const cur = this.current();
353
+ this._limited.add(cur.type);
354
+ const startIdx = this._idx;
355
+ for (let i = 1; i < this._list.length; i++) {
356
+ const nextIdx = (startIdx + i) % this._list.length;
357
+ const nextType = this._list[nextIdx].type;
358
+ if (!this._limited.has(nextType)) {
359
+ this._idx = nextIdx;
360
+ this._applyCurrent();
361
+ return true;
362
+ }
363
+ }
364
+ return false;
365
+ }
366
+
367
+ clearLimits() {
368
+ this._limited.clear();
369
+ this._applyCurrent();
370
+ }
371
+
372
+ _detectType(key) {
373
+ for (const [type, def] of Object.entries(PROVIDERS)) {
374
+ if (def.detectKey && def.detectKey(key)) return type;
375
+ }
376
+ return 'openrouter';
377
+ }
378
+
379
+ static detectType(key) {
380
+ for (const [type, def] of Object.entries(PROVIDERS)) {
381
+ if (def.detectKey && def.detectKey(key)) return type;
382
+ }
383
+ return 'openrouter';
384
+ }
385
+ }
386
+
387
+ module.exports = { ProviderManager, PROVIDERS };
package/src/setup.js ADDED
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const blessed = require('blessed');
4
+
5
+ function ew(s) {
6
+ return String(s).replace(/\{/g, '\\{').replace(/\}/g, '\\}');
7
+ }
8
+
9
+ // ── First-run setup — simple API key paste ─────────────────────────────────
10
+
11
+ function showSetup(screen) {
12
+ return new Promise((resolve) => {
13
+ const W = Math.min(screen.width - 4, 60);
14
+ const H = Math.min(screen.height - 2, 22);
15
+
16
+ const overlay = blessed.box({
17
+ parent: screen,
18
+ top: 'center', left: 'center',
19
+ width: W, height: H,
20
+ tags: true,
21
+ border: { type: 'line' },
22
+ style: { fg: '#00ff88', bg: '#000a00', border: { fg: '#00aa44' } },
23
+ padding: { left: 2, right: 2 },
24
+ });
25
+
26
+ overlay.setContent([
27
+ '',
28
+ '{center}{green-fg}{bold}◈ Vibe Hacker — Get Started{/bold}{/green-fg}{/center}',
29
+ '',
30
+ '{center}{#888888-fg}Enter your API key to begin.{/#888888-fg}{/center}',
31
+ '{center}{#888888-fg}Get a free key at vibsecurity.com{/#888888-fg}{/center}',
32
+ '',
33
+ '{#003300-fg} ════════════════════════════════════════════{/#003300-fg}',
34
+ '',
35
+ ' {#555555-fg}Supported keys:{/#555555-fg}',
36
+ ' {#44ff88-fg}Vibe Hacker{/#44ff88-fg} {#666666-fg}vh_… / sk-or-v1-…{/#666666-fg}',
37
+ ' {#44ff88-fg}Groq{/#44ff88-fg} {#666666-fg}gsk_…{/#666666-fg}',
38
+ ' {#44ff88-fg}Gemini{/#44ff88-fg} {#666666-fg}AIza…{/#666666-fg}',
39
+ ' {#44ff88-fg}Cerebras{/#44ff88-fg} {#666666-fg}csk-…{/#666666-fg}',
40
+ ' {#44ff88-fg}OpenAI{/#44ff88-fg} {#666666-fg}sk-…{/#666666-fg}',
41
+ ' {#44ff88-fg}Anthropic{/#44ff88-fg} {#666666-fg}sk-ant-…{/#666666-fg}',
42
+ '',
43
+ ].join('\n'));
44
+
45
+ const input = blessed.textbox({
46
+ parent: overlay,
47
+ bottom: 1, left: 2, right: 2, height: 3,
48
+ border: { type: 'line' },
49
+ label: ' {#00ff88-fg}Paste API Key{/#00ff88-fg} ',
50
+ style: { fg: '#00ff88', bg: '#001100', border: { fg: '#00aa44' } },
51
+ inputOnFocus: false,
52
+ mouse: true,
53
+ });
54
+
55
+ const statusLine = blessed.text({
56
+ parent: overlay,
57
+ bottom: 0, left: 2, right: 2, height: 1,
58
+ tags: true,
59
+ content: '',
60
+ style: { fg: '#00aa44', bg: 'black' },
61
+ });
62
+
63
+ overlay.focus();
64
+ screen.render();
65
+
66
+ setTimeout(() => {
67
+ input.focus();
68
+ input.readInput();
69
+ screen.render();
70
+ }, 50);
71
+
72
+ input.on('submit', (value) => {
73
+ const key = (value || '').trim();
74
+ if (!key || key.length < 10) {
75
+ statusLine.setContent(' {red-fg}Key too short — paste a valid API key{/red-fg}');
76
+ input.clearValue();
77
+ input.focus();
78
+ input.readInput();
79
+ screen.render();
80
+ return;
81
+ }
82
+ try { input.destroy(); overlay.destroy(); } catch (_) {}
83
+ screen.render();
84
+ resolve(key);
85
+ });
86
+
87
+ input.on('cancel', () => {
88
+ try { input.destroy(); overlay.destroy(); } catch (_) {}
89
+ screen.render();
90
+ resolve(null);
91
+ });
92
+ });
93
+ }
94
+
95
+ module.exports = { showSetup };