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.
- package/LICENSE +30 -0
- package/README.md +77 -0
- package/index.js +37 -0
- package/package.json +51 -0
- package/src/agent.js +311 -0
- package/src/api.js +314 -0
- package/src/app.js +2198 -0
- package/src/approve.js +217 -0
- package/src/auth.js +45 -0
- package/src/config.js +59 -0
- package/src/models.js +218 -0
- package/src/providers.js +387 -0
- package/src/setup.js +95 -0
- package/src/supabase.js +287 -0
- package/src/tools.js +588 -0
- package/src/welcome.js +119 -0
package/src/approve.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const blessed = require('blessed');
|
|
4
|
+
|
|
5
|
+
// ── Tool descriptions for the approval dialog ─────────────────────────────────
|
|
6
|
+
const TOOL_VERB = {
|
|
7
|
+
write_file: 'Write file',
|
|
8
|
+
edit_file: 'Edit file',
|
|
9
|
+
execute_command: 'Run command',
|
|
10
|
+
delete_file: 'Delete file',
|
|
11
|
+
create_directory: 'Create directory',
|
|
12
|
+
read_file: 'Read file',
|
|
13
|
+
list_files: 'List files',
|
|
14
|
+
search_files: 'Search files',
|
|
15
|
+
grep: 'Search contents',
|
|
16
|
+
glob: 'Find files',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Tools that need approval (risky)
|
|
20
|
+
const NEEDS_APPROVAL = new Set([
|
|
21
|
+
'write_file', 'edit_file', 'execute_command', 'delete_file',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
// ── Format tool args into a human-readable description ────────────────────────
|
|
25
|
+
function describeToolCall(tc) {
|
|
26
|
+
const { name, args } = tc;
|
|
27
|
+
if (name === 'write_file') return args.path || '(unknown path)';
|
|
28
|
+
if (name === 'execute_command') return args.command || '(unknown command)';
|
|
29
|
+
if (name === 'delete_file') return args.path || '(unknown path)';
|
|
30
|
+
if (name === 'create_directory') return args.path || '(unknown path)';
|
|
31
|
+
if (name === 'read_file') return args.path || '(unknown path)';
|
|
32
|
+
if (name === 'list_files') return args.path || '.';
|
|
33
|
+
if (name === 'search_files') return `"${args.pattern || ''}" in ${args.path || '.'}`;
|
|
34
|
+
return JSON.stringify(args).substring(0, 80);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get the path/command for "always allow" matching
|
|
38
|
+
function getAllowKey(tc) {
|
|
39
|
+
const { name, args } = tc;
|
|
40
|
+
if (name === 'execute_command') {
|
|
41
|
+
// Allow by command prefix (first word)
|
|
42
|
+
const cmd = (args.command || '').trim().split(/\s+/)[0];
|
|
43
|
+
return `cmd:${cmd}`;
|
|
44
|
+
}
|
|
45
|
+
// Allow by directory for file ops
|
|
46
|
+
const p = args.path || '';
|
|
47
|
+
const parts = p.replace(/\\/g, '/').split('/');
|
|
48
|
+
return 'path:' + parts.slice(0, -1).join('/'); // parent dir
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Main dialog ───────────────────────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Show tool approval dialog.
|
|
54
|
+
* Returns Promise<'yes' | 'always' | 'no'>
|
|
55
|
+
*
|
|
56
|
+
* @param {object} screen - blessed screen
|
|
57
|
+
* @param {object} tc - tool call { name, args }
|
|
58
|
+
* @param {string} context - optional one-line AI description of what it's doing
|
|
59
|
+
*/
|
|
60
|
+
function showApproval(screen, tc, context) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
const verb = TOOL_VERB[tc.name] || tc.name;
|
|
63
|
+
const detail = describeToolCall(tc);
|
|
64
|
+
const allowKey = getAllowKey(tc);
|
|
65
|
+
const isCmd = tc.name === 'execute_command';
|
|
66
|
+
|
|
67
|
+
// "Yes, and always allow…" label
|
|
68
|
+
const alwaysLabel = isCmd
|
|
69
|
+
? `Yes, and always allow {bold}${(tc.args.command || '').split(/\s+/)[0]}{/bold} commands`
|
|
70
|
+
: `Yes, and always allow writes to {bold}${(detail.replace(/\\/g,'/').split('/').slice(0,-1).join('/') || detail)}{/bold}`;
|
|
71
|
+
|
|
72
|
+
const items = [
|
|
73
|
+
{ key: '1', label: 'Yes', value: 'yes', color: '#00ff88' },
|
|
74
|
+
{ key: '2', label: alwaysLabel, value: 'always', color: '#88ff44' },
|
|
75
|
+
{ key: '3', label: 'No', value: 'no', color: '#ff4444' },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const W = Math.min(Math.max(40, screen.width - 6), 70);
|
|
79
|
+
|
|
80
|
+
// Fixed chrome: border(2) + blank(1) + header(1) + detail(1) + blank(1)
|
|
81
|
+
// + "Do you want to proceed?"(1) + blank(1) + buttons(items.length)
|
|
82
|
+
// + blank(1) + hint(1) = 10 + items.length
|
|
83
|
+
// Everything else (context line + preview) must fit in remaining space.
|
|
84
|
+
const maxH = Math.max(10, screen.height - 4);
|
|
85
|
+
const fixedH = 10 + items.length;
|
|
86
|
+
const budget = Math.max(0, maxH - fixedH);
|
|
87
|
+
|
|
88
|
+
// Context eats 1 line if present
|
|
89
|
+
const ctxLine = context ? `{#444444-fg}${esc(context)}{/#444444-fg}` : '';
|
|
90
|
+
const ctxCost = ctxLine ? 1 : 0;
|
|
91
|
+
|
|
92
|
+
// Preview: clamp to remaining budget (separator + lines)
|
|
93
|
+
let preview = '';
|
|
94
|
+
if (tc.name === 'write_file' && tc.args.content) {
|
|
95
|
+
preview = tc.args.content;
|
|
96
|
+
} else if (tc.name === 'execute_command') {
|
|
97
|
+
preview = tc.args.command || '';
|
|
98
|
+
}
|
|
99
|
+
const previewBudget = Math.max(0, budget - ctxCost - (preview ? 1 : 0)); // -1 for separator
|
|
100
|
+
if (preview) {
|
|
101
|
+
const allLines = preview.split('\n');
|
|
102
|
+
if (allLines.length > previewBudget) {
|
|
103
|
+
preview = allLines.slice(0, Math.max(1, previewBudget - 1)).join('\n') + '\n…';
|
|
104
|
+
}
|
|
105
|
+
// Also hard-cap to 6 lines regardless, to keep dialog compact
|
|
106
|
+
const cappedLines = preview.split('\n');
|
|
107
|
+
if (cappedLines.length > 6) {
|
|
108
|
+
preview = cappedLines.slice(0, 5).join('\n') + '\n…';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const previewCost = preview ? preview.split('\n').length + 1 : 0; // +1 separator
|
|
112
|
+
|
|
113
|
+
const H = Math.min(maxH, fixedH + ctxCost + previewCost);
|
|
114
|
+
|
|
115
|
+
const box = blessed.box({
|
|
116
|
+
parent: screen,
|
|
117
|
+
top: 'center',
|
|
118
|
+
left: 'center',
|
|
119
|
+
width: W,
|
|
120
|
+
height: Math.min(H, screen.height - 4),
|
|
121
|
+
tags: true,
|
|
122
|
+
border: { type: 'line' },
|
|
123
|
+
shadow: false,
|
|
124
|
+
style: {
|
|
125
|
+
fg: '#00ff88', bg: '#000a00',
|
|
126
|
+
border: { fg: '#00aa44' },
|
|
127
|
+
},
|
|
128
|
+
keys: true,
|
|
129
|
+
mouse: true,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function buildContent(selectedIdx) {
|
|
133
|
+
const lines = [];
|
|
134
|
+
lines.push('');
|
|
135
|
+
|
|
136
|
+
// Header
|
|
137
|
+
lines.push(` {bold}{#00ff88-fg}${esc(verb)}{/}{/bold}`);
|
|
138
|
+
|
|
139
|
+
// Detail (path / command)
|
|
140
|
+
lines.push(` {#88ff88-fg}${esc(detail)}{/#88ff88-fg}`);
|
|
141
|
+
|
|
142
|
+
// Context
|
|
143
|
+
if (ctxLine) lines.push(` ${ctxLine}`);
|
|
144
|
+
|
|
145
|
+
// Preview block
|
|
146
|
+
if (preview) {
|
|
147
|
+
lines.push(` {#333333-fg}${'─'.repeat(W - 4)}{/#333333-fg}`);
|
|
148
|
+
preview.split('\n').forEach(l => {
|
|
149
|
+
lines.push(` {#555555-fg}${esc(l.substring(0, W - 4))}{/#555555-fg}`);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
lines.push('');
|
|
154
|
+
lines.push(` {#888888-fg}Do you want to proceed?{/#888888-fg}`);
|
|
155
|
+
lines.push('');
|
|
156
|
+
|
|
157
|
+
// Menu items
|
|
158
|
+
items.forEach((item, i) => {
|
|
159
|
+
const cursor = i === selectedIdx ? '{yellow-fg}❯{/yellow-fg}' : ' ';
|
|
160
|
+
const numBadge = `{#444444-fg}${item.key}.{/#444444-fg}`;
|
|
161
|
+
const labelStr = i === selectedIdx
|
|
162
|
+
? `{bold}{${item.color}-fg}${item.label}{/}{/bold}`
|
|
163
|
+
: `{#888888-fg}${item.label}{/#888888-fg}`;
|
|
164
|
+
lines.push(` ${cursor} ${numBadge} ${labelStr}`);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
lines.push('');
|
|
168
|
+
lines.push(` {#333333-fg}Esc cancel · ↑↓ navigate · Enter confirm{/#333333-fg}`);
|
|
169
|
+
|
|
170
|
+
return lines.join('\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let selectedIdx = 0;
|
|
174
|
+
box.setContent(buildContent(selectedIdx));
|
|
175
|
+
box.focus();
|
|
176
|
+
screen.render();
|
|
177
|
+
|
|
178
|
+
function done(value) {
|
|
179
|
+
try { box.destroy(); } catch (_) {}
|
|
180
|
+
screen.render();
|
|
181
|
+
resolve(value);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Keyboard navigation
|
|
185
|
+
box.on('keypress', (ch, key) => {
|
|
186
|
+
if (!key) return;
|
|
187
|
+
const name = key.name;
|
|
188
|
+
|
|
189
|
+
if (name === 'escape') { done('no'); return; }
|
|
190
|
+
if (name === 'up') { selectedIdx = Math.max(0, selectedIdx - 1); box.setContent(buildContent(selectedIdx)); screen.render(); return; }
|
|
191
|
+
if (name === 'down') { selectedIdx = Math.min(items.length - 1, selectedIdx + 1); box.setContent(buildContent(selectedIdx)); screen.render(); return; }
|
|
192
|
+
if (name === 'enter' || name === 'return') { done(items[selectedIdx].value); return; }
|
|
193
|
+
|
|
194
|
+
// Number shortcuts
|
|
195
|
+
if (ch === '1') { done('yes'); return; }
|
|
196
|
+
if (ch === '2') { done('always'); return; }
|
|
197
|
+
if (ch === '3') { done('no'); return; }
|
|
198
|
+
if (ch === 'y' || ch === 'Y') { done('yes'); return; }
|
|
199
|
+
if (ch === 'n' || ch === 'N') { done('no'); return; }
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Mouse click on items
|
|
203
|
+
items.forEach((item, i) => {
|
|
204
|
+
box.on('click', () => {
|
|
205
|
+
done(items[selectedIdx].value);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
screen.render();
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function esc(str) {
|
|
214
|
+
return String(str).replace(/\{/g, '\\{').replace(/\}/g, '\\}');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = { showApproval, NEEDS_APPROVAL, getAllowKey };
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Vibe Hacker Auth — simple API key entry ──────────────────────────────
|
|
4
|
+
// No browser popup. User gets key from vibsecurity.com, pastes it.
|
|
5
|
+
|
|
6
|
+
const LOGIN_URL = process.env.VH_LOGIN_URL || 'https://vibsecurity.com/login';
|
|
7
|
+
|
|
8
|
+
function openBrowser(url) {
|
|
9
|
+
if (typeof url !== 'string' || !/^https?:\/\//i.test(url)) {
|
|
10
|
+
return Promise.resolve(false);
|
|
11
|
+
}
|
|
12
|
+
const { spawn } = require('child_process');
|
|
13
|
+
const platform = process.platform;
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
let proc;
|
|
16
|
+
try {
|
|
17
|
+
if (platform === 'win32') {
|
|
18
|
+
proc = spawn('cmd.exe', ['/c', 'start', '""', url], {
|
|
19
|
+
detached: true, stdio: 'ignore', windowsVerbatimArguments: false,
|
|
20
|
+
});
|
|
21
|
+
} else if (platform === 'darwin') {
|
|
22
|
+
proc = spawn('open', [url], { detached: true, stdio: 'ignore' });
|
|
23
|
+
} else {
|
|
24
|
+
proc = spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
|
|
25
|
+
proc.on('error', () => {
|
|
26
|
+
try {
|
|
27
|
+
const fb = spawn('wslview', [url], { detached: true, stdio: 'ignore' });
|
|
28
|
+
fb.on('error', () => resolve(false));
|
|
29
|
+
fb.unref();
|
|
30
|
+
resolve(true);
|
|
31
|
+
} catch (_) { resolve(false); }
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (proc) {
|
|
35
|
+
proc.on('error', () => resolve(false));
|
|
36
|
+
proc.unref();
|
|
37
|
+
setTimeout(() => resolve(true), 100);
|
|
38
|
+
}
|
|
39
|
+
} catch (_) {
|
|
40
|
+
resolve(false);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { openBrowser, LOGIN_URL };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), '.vibehacker');
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
9
|
+
|
|
10
|
+
// Migrate from old ~/.hackerai config if it exists
|
|
11
|
+
try {
|
|
12
|
+
const oldDir = path.join(os.homedir(), '.hackerai');
|
|
13
|
+
const oldFile = path.join(oldDir, 'config.json');
|
|
14
|
+
if (fs.existsSync(oldFile) && !fs.existsSync(CONFIG_FILE)) {
|
|
15
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
16
|
+
fs.copyFileSync(oldFile, CONFIG_FILE);
|
|
17
|
+
}
|
|
18
|
+
} catch (_) {}
|
|
19
|
+
|
|
20
|
+
// Load once at startup — no repeated fs reads
|
|
21
|
+
let _saved;
|
|
22
|
+
try {
|
|
23
|
+
_saved = fs.existsSync(CONFIG_FILE) ? JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) : {};
|
|
24
|
+
} catch (_) { _saved = {}; }
|
|
25
|
+
|
|
26
|
+
function saveConfig(data) {
|
|
27
|
+
try {
|
|
28
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
29
|
+
// Merge with in-memory saved state (not disk — avoid re-read)
|
|
30
|
+
Object.assign(_saved, data);
|
|
31
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(_saved, null, 2));
|
|
32
|
+
} catch (_) {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const config = {
|
|
36
|
+
apiKey: _saved.apiKey || '',
|
|
37
|
+
baseURL: _saved.baseURL || 'https://openrouter.ai/api/v1',
|
|
38
|
+
appName: 'Vibe Hacker',
|
|
39
|
+
version: '4.1.0',
|
|
40
|
+
noLogging: true,
|
|
41
|
+
maxTokens: 8192,
|
|
42
|
+
temperature: 0.6, // slightly lower for more reliable tool use
|
|
43
|
+
maxToolIterations: 30, // generous for complex tasks
|
|
44
|
+
httpReferer: 'https://vibsecurity.com',
|
|
45
|
+
xTitle: 'Vibe Hacker',
|
|
46
|
+
|
|
47
|
+
providers: _saved.providers || [],
|
|
48
|
+
|
|
49
|
+
hasKey() {
|
|
50
|
+
return !!(this.apiKey && this.apiKey.length > 10);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
setApiKey(key) {
|
|
54
|
+
this.apiKey = key;
|
|
55
|
+
saveConfig({ apiKey: key });
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports = config;
|
package/src/models.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Built-in free models. Run /refresh to update from API.
|
|
4
|
+
// Ordered by speed — faster models first for better default experience.
|
|
5
|
+
const FREE_MODELS = [
|
|
6
|
+
{
|
|
7
|
+
id: 'meta-llama/llama-3.3-70b-instruct:free',
|
|
8
|
+
name: 'llama-3.3-70b',
|
|
9
|
+
provider: 'Meta',
|
|
10
|
+
description: 'Llama 3.3 70B — fast & capable',
|
|
11
|
+
contextWindow: 131072,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'qwen/qwen3-coder:free',
|
|
15
|
+
name: 'qwen3-coder',
|
|
16
|
+
provider: 'Qwen',
|
|
17
|
+
description: 'Dedicated coding model — 262k context',
|
|
18
|
+
contextWindow: 262000,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'qwen/qwen3.6-plus:free',
|
|
22
|
+
name: 'qwen3.6-plus',
|
|
23
|
+
provider: 'Qwen',
|
|
24
|
+
description: 'Qwen 3.6 Plus — 1M context, very capable',
|
|
25
|
+
contextWindow: 1000000,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'qwen/qwen3-next-80b-a3b-instruct:free',
|
|
29
|
+
name: 'qwen3-next-80b',
|
|
30
|
+
provider: 'Qwen',
|
|
31
|
+
description: 'Qwen3 Next 80B instruction model',
|
|
32
|
+
contextWindow: 262144,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'nvidia/nemotron-3-super-120b-a12b:free',
|
|
36
|
+
name: 'nemotron-120b',
|
|
37
|
+
provider: 'NVIDIA',
|
|
38
|
+
description: 'Nemotron 3 Super 120B — 262k context',
|
|
39
|
+
contextWindow: 262144,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'nvidia/nemotron-3-nano-30b-a3b:free',
|
|
43
|
+
name: 'nemotron-30b',
|
|
44
|
+
provider: 'NVIDIA',
|
|
45
|
+
description: 'Nemotron 3 Nano 30B — 256k context',
|
|
46
|
+
contextWindow: 256000,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'nvidia/nemotron-nano-12b-v2-vl:free',
|
|
50
|
+
name: 'nemotron-12b-vl',
|
|
51
|
+
provider: 'NVIDIA',
|
|
52
|
+
description: 'Nemotron Nano 12B vision-language',
|
|
53
|
+
contextWindow: 128000,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'nvidia/nemotron-nano-9b-v2:free',
|
|
57
|
+
name: 'nemotron-9b',
|
|
58
|
+
provider: 'NVIDIA',
|
|
59
|
+
description: 'Nemotron Nano 9B — 128k context',
|
|
60
|
+
contextWindow: 128000,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'meta-llama/llama-3.2-3b-instruct:free',
|
|
64
|
+
name: 'llama-3.2-3b',
|
|
65
|
+
provider: 'Meta',
|
|
66
|
+
description: 'Llama 3.2 3B — fast & lightweight',
|
|
67
|
+
contextWindow: 131072,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'nousresearch/hermes-3-llama-3.1-405b:free',
|
|
71
|
+
name: 'hermes-3-405b',
|
|
72
|
+
provider: 'Nous',
|
|
73
|
+
description: 'Hermes 3 on Llama 405B — very capable',
|
|
74
|
+
contextWindow: 131072,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'google/gemma-3-27b-it:free',
|
|
78
|
+
name: 'gemma-3-27b',
|
|
79
|
+
provider: 'Google',
|
|
80
|
+
description: 'Gemma 3 27B instruction-tuned',
|
|
81
|
+
contextWindow: 131072,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'google/gemma-3-12b-it:free',
|
|
85
|
+
name: 'gemma-3-12b',
|
|
86
|
+
provider: 'Google',
|
|
87
|
+
description: 'Gemma 3 12B instruction-tuned',
|
|
88
|
+
contextWindow: 32768,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'google/gemma-3-4b-it:free',
|
|
92
|
+
name: 'gemma-3-4b',
|
|
93
|
+
provider: 'Google',
|
|
94
|
+
description: 'Gemma 3 4B — fast & compact',
|
|
95
|
+
contextWindow: 32768,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'arcee-ai/trinity-large-preview:free',
|
|
99
|
+
name: 'trinity-large',
|
|
100
|
+
provider: 'Arcee',
|
|
101
|
+
description: 'Trinity Large Preview — 131k context',
|
|
102
|
+
contextWindow: 131000,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'arcee-ai/trinity-mini:free',
|
|
106
|
+
name: 'trinity-mini',
|
|
107
|
+
provider: 'Arcee',
|
|
108
|
+
description: 'Trinity Mini — fast, 131k context',
|
|
109
|
+
contextWindow: 131072,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'minimax/minimax-m2.5:free',
|
|
113
|
+
name: 'minimax-m2.5',
|
|
114
|
+
provider: 'MiniMax',
|
|
115
|
+
description: 'MiniMax M2.5 — 196k context',
|
|
116
|
+
contextWindow: 196608,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'stepfun/step-3.5-flash:free',
|
|
120
|
+
name: 'step-3.5-flash',
|
|
121
|
+
provider: 'StepFun',
|
|
122
|
+
description: 'Step 3.5 Flash — fast, 256k context',
|
|
123
|
+
contextWindow: 256000,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'z-ai/glm-4.5-air:free',
|
|
127
|
+
name: 'glm-4.5-air',
|
|
128
|
+
provider: 'Z-AI',
|
|
129
|
+
description: 'GLM 4.5 Air — 131k context',
|
|
130
|
+
contextWindow: 131072,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',
|
|
134
|
+
name: 'dolphin-24b',
|
|
135
|
+
provider: 'CogComp',
|
|
136
|
+
description: 'Dolphin Mistral 24B Venice Edition',
|
|
137
|
+
contextWindow: 32768,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'liquid/lfm-2.5-1.2b-thinking:free',
|
|
141
|
+
name: 'lfm-thinking-1.2b',
|
|
142
|
+
provider: 'Liquid',
|
|
143
|
+
description: 'LFM 2.5 1.2B with thinking mode',
|
|
144
|
+
contextWindow: 32768,
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
class ModelManager {
|
|
149
|
+
constructor() {
|
|
150
|
+
this.models = FREE_MODELS;
|
|
151
|
+
this.currentIndex = 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
current() {
|
|
155
|
+
return this.models[this.currentIndex];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
next() {
|
|
159
|
+
this.currentIndex = (this.currentIndex + 1) % this.models.length;
|
|
160
|
+
return this.current();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
prev() {
|
|
164
|
+
this.currentIndex = (this.currentIndex - 1 + this.models.length) % this.models.length;
|
|
165
|
+
return this.current();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
selectById(id) {
|
|
169
|
+
const idx = this.models.findIndex(m => m.id === id);
|
|
170
|
+
if (idx !== -1) {
|
|
171
|
+
this.currentIndex = idx;
|
|
172
|
+
return this.current();
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
list() {
|
|
178
|
+
return this.models;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Refresh model list from API — replaces list with all live free models
|
|
182
|
+
async refresh(apiKey, baseURL) {
|
|
183
|
+
try {
|
|
184
|
+
const axios = require('axios');
|
|
185
|
+
const r = await axios.get(`${baseURL}/models`, {
|
|
186
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
187
|
+
timeout: 15000,
|
|
188
|
+
});
|
|
189
|
+
const allModels = r.data?.data || [];
|
|
190
|
+
// Get all free models from API
|
|
191
|
+
const live = allModels.filter(m =>
|
|
192
|
+
m.id.endsWith(':free') && m.pricing?.prompt === '0'
|
|
193
|
+
);
|
|
194
|
+
if (live.length > 0) {
|
|
195
|
+
// Sort by context window (largest first), then alphabetically
|
|
196
|
+
live.sort((a, b) => (b.context_length || 0) - (a.context_length || 0) || a.id.localeCompare(b.id));
|
|
197
|
+
|
|
198
|
+
const currentId = this.models[this.currentIndex]?.id;
|
|
199
|
+
this.models = live.map(m => ({
|
|
200
|
+
id: m.id,
|
|
201
|
+
name: m.id.split('/')[1].replace(':free', ''),
|
|
202
|
+
provider: m.id.split('/')[0],
|
|
203
|
+
description: m.name || m.id,
|
|
204
|
+
contextWindow: m.context_length || 32768,
|
|
205
|
+
free: true,
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
// Try to keep current selection
|
|
209
|
+
const newIdx = this.models.findIndex(m => m.id === currentId);
|
|
210
|
+
this.currentIndex = newIdx >= 0 ? newIdx : 0;
|
|
211
|
+
return this.models.length;
|
|
212
|
+
}
|
|
213
|
+
} catch (_) {}
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = { ModelManager, FREE_MODELS };
|