vibehacker 4.1.0 → 4.2.1
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/README.md +381 -43
- package/package.json +1 -1
- package/src/app.js +29 -76
- package/src/approve.js +12 -17
- package/src/providers.js +2 -2
- package/src/src/agent.js +311 -0
- package/src/src/api.js +314 -0
- package/src/src/app.js +2196 -0
- package/src/src/approve.js +212 -0
- package/src/src/auth.js +45 -0
- package/src/src/config.js +59 -0
- package/src/src/models.js +218 -0
- package/src/src/providers.js +387 -0
- package/src/src/setup.js +108 -0
- package/src/src/supabase.js +287 -0
- package/src/src/tools.js +588 -0
- package/src/src/welcome.js +119 -0
package/src/approve.js
CHANGED
|
@@ -25,6 +25,7 @@ const NEEDS_APPROVAL = new Set([
|
|
|
25
25
|
function describeToolCall(tc) {
|
|
26
26
|
const { name, args } = tc;
|
|
27
27
|
if (name === 'write_file') return args.path || '(unknown path)';
|
|
28
|
+
if (name === 'edit_file') return args.path || '(unknown path)';
|
|
28
29
|
if (name === 'execute_command') return args.command || '(unknown command)';
|
|
29
30
|
if (name === 'delete_file') return args.path || '(unknown path)';
|
|
30
31
|
if (name === 'create_directory') return args.path || '(unknown path)';
|
|
@@ -77,40 +78,34 @@ function showApproval(screen, tc, context) {
|
|
|
77
78
|
|
|
78
79
|
const W = Math.min(Math.max(40, screen.width - 6), 70);
|
|
79
80
|
|
|
80
|
-
// Fixed chrome:
|
|
81
|
-
// +
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const budget = Math.max(0, maxH - fixedH);
|
|
81
|
+
// Fixed chrome lines:
|
|
82
|
+
// border-top(1) + blank(1) + header(1) + detail(1) + blank(1)
|
|
83
|
+
// + "Do you want to proceed?"(1) + blank(1) + buttons(3)
|
|
84
|
+
// + blank(1) + hint(1) + border-bottom(1) = 13
|
|
85
|
+
const CHROME = 13;
|
|
86
|
+
const maxH = Math.max(CHROME, screen.height - 2);
|
|
87
87
|
|
|
88
88
|
// Context eats 1 line if present
|
|
89
89
|
const ctxLine = context ? `{#444444-fg}${esc(context)}{/#444444-fg}` : '';
|
|
90
90
|
const ctxCost = ctxLine ? 1 : 0;
|
|
91
91
|
|
|
92
|
-
// Preview:
|
|
92
|
+
// Preview: hard-cap to 4 lines max — buttons MUST always be visible
|
|
93
93
|
let preview = '';
|
|
94
94
|
if (tc.name === 'write_file' && tc.args.content) {
|
|
95
95
|
preview = tc.args.content;
|
|
96
96
|
} else if (tc.name === 'execute_command') {
|
|
97
97
|
preview = tc.args.command || '';
|
|
98
98
|
}
|
|
99
|
-
const previewBudget = Math.max(0, budget - ctxCost - (preview ? 1 : 0)); // -1 for separator
|
|
100
99
|
if (preview) {
|
|
101
100
|
const allLines = preview.split('\n');
|
|
102
|
-
|
|
103
|
-
|
|
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…';
|
|
101
|
+
const maxPreviewLines = Math.min(4, Math.max(0, maxH - CHROME - ctxCost - 1));
|
|
102
|
+
if (allLines.length > maxPreviewLines) {
|
|
103
|
+
preview = allLines.slice(0, Math.max(1, maxPreviewLines - 1)).join('\n') + '\n…';
|
|
109
104
|
}
|
|
110
105
|
}
|
|
111
106
|
const previewCost = preview ? preview.split('\n').length + 1 : 0; // +1 separator
|
|
112
107
|
|
|
113
|
-
const H = Math.min(maxH,
|
|
108
|
+
const H = Math.min(maxH, CHROME + ctxCost + previewCost);
|
|
114
109
|
|
|
115
110
|
const box = blessed.box({
|
|
116
111
|
parent: screen,
|
package/src/providers.js
CHANGED
|
@@ -10,7 +10,7 @@ const PROVIDERS = {
|
|
|
10
10
|
name: 'Vibe Hacker',
|
|
11
11
|
shortName: 'Vibe Hacker',
|
|
12
12
|
baseURL: VH_GATEWAY_URL,
|
|
13
|
-
freeNote: '
|
|
13
|
+
freeNote: '50 req/day free',
|
|
14
14
|
tier: 'free',
|
|
15
15
|
detectKey: k => k.startsWith('vh_'),
|
|
16
16
|
getKey: () => 'Sign up at https://vibsecurity.com',
|
|
@@ -29,7 +29,7 @@ const PROVIDERS = {
|
|
|
29
29
|
name: 'Vibe Hacker',
|
|
30
30
|
shortName: 'Vibe Hacker',
|
|
31
31
|
baseURL: 'https://openrouter.ai/api/v1',
|
|
32
|
-
freeNote: '
|
|
32
|
+
freeNote: '50 req/day free',
|
|
33
33
|
tier: 'free',
|
|
34
34
|
detectKey: k => k.startsWith('sk-or-v1-'),
|
|
35
35
|
getKey: () => 'vibsecurity.com/login',
|
package/src/src/agent.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { streamChat } = require('./api');
|
|
6
|
+
const { parseToolCalls, executeTool, TOOL_DOCS } = require('./tools');
|
|
7
|
+
const config = require('./config');
|
|
8
|
+
|
|
9
|
+
// ── Modes ────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const MODES = [
|
|
12
|
+
{ id: 'chat', name: 'Chat', description: 'Security Q&A and threat intel' },
|
|
13
|
+
{ id: 'hunt', name: 'Hunt', description: 'Autonomous coding, security ops & tool use' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// ── Project Memory ───────────────────────────────────────────────────────────
|
|
17
|
+
// Cached — only reads filesystem once per cwd, refreshes when cwd changes.
|
|
18
|
+
|
|
19
|
+
let _memCache = { cwd: null, content: null };
|
|
20
|
+
|
|
21
|
+
function loadProjectMemory(cwd) {
|
|
22
|
+
if (_memCache.cwd === cwd) return _memCache.content;
|
|
23
|
+
|
|
24
|
+
const candidates = [
|
|
25
|
+
path.join(cwd, 'VIBEHACKER.md'),
|
|
26
|
+
path.join(cwd, '.vibehacker', 'context.md'),
|
|
27
|
+
path.join(cwd, '.vibehacker', 'instructions.md'),
|
|
28
|
+
];
|
|
29
|
+
let result = null;
|
|
30
|
+
for (const f of candidates) {
|
|
31
|
+
try {
|
|
32
|
+
const c = fs.readFileSync(f, 'utf8');
|
|
33
|
+
if (c.trim()) { result = { file: path.relative(cwd, f), content: c.trim() }; break; }
|
|
34
|
+
} catch (_) {}
|
|
35
|
+
}
|
|
36
|
+
_memCache = { cwd, content: result };
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── System Prompt — Cached, Only Rebuilt When cwd/mode Changes ───────────────
|
|
41
|
+
|
|
42
|
+
let _promptCache = { mode: null, cwd: null, prompt: null };
|
|
43
|
+
|
|
44
|
+
function buildSystemPrompt(mode, cwd) {
|
|
45
|
+
if (_promptCache.mode === mode && _promptCache.cwd === cwd && _promptCache.prompt) {
|
|
46
|
+
return _promptCache.prompt;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const os = process.platform === 'win32' ? 'Windows' : process.platform === 'darwin' ? 'macOS' : 'Linux';
|
|
50
|
+
const shell = process.platform === 'win32' ? 'powershell' : 'bash';
|
|
51
|
+
const date = new Date().toISOString().split('T')[0];
|
|
52
|
+
|
|
53
|
+
const projectMem = loadProjectMemory(cwd);
|
|
54
|
+
const projectSection = projectMem
|
|
55
|
+
? `\n\n# Project Instructions (${projectMem.file})\n${projectMem.content}\n`
|
|
56
|
+
: '';
|
|
57
|
+
|
|
58
|
+
let prompt;
|
|
59
|
+
|
|
60
|
+
if (mode === 'hunt') {
|
|
61
|
+
prompt = `You are Vibe Hacker v${config.version} — an expert autonomous AI agent.
|
|
62
|
+
|
|
63
|
+
# Environment
|
|
64
|
+
- CWD: ${cwd}
|
|
65
|
+
- OS: ${os} | Shell: ${shell} | Date: ${date}
|
|
66
|
+
${projectSection}
|
|
67
|
+
# HUNT MODE — Autonomous Agent with Tool Access
|
|
68
|
+
|
|
69
|
+
You have filesystem + shell access via XML tool blocks. DO the work — don't describe it.
|
|
70
|
+
|
|
71
|
+
${TOOL_DOCS}
|
|
72
|
+
|
|
73
|
+
# Rules (MANDATORY)
|
|
74
|
+
|
|
75
|
+
1. USE TOOLS. Don't explain what you'd do — DO IT with tool calls.
|
|
76
|
+
✗ "You could run npm install" → ✓ <execute_command><command>npm install</command></execute_command>
|
|
77
|
+
|
|
78
|
+
2. READ BEFORE EDIT. Always read_file before edit_file. The tool rejects edits on unread files.
|
|
79
|
+
|
|
80
|
+
3. EDIT > WRITE. Modify existing files with edit_file (surgical replacement). Only write_file for NEW files.
|
|
81
|
+
|
|
82
|
+
4. EXACT MATCHING. edit_file old_string must match the file exactly — whitespace, indentation, everything.
|
|
83
|
+
If it fails: read the file again, the content changed. Add more surrounding context if not unique.
|
|
84
|
+
|
|
85
|
+
5. MULTIPLE TOOLS OK. Use several tools in one response when they're independent.
|
|
86
|
+
|
|
87
|
+
6. GREP > MANUAL SEARCH. Use grep/glob to find code. Don't read every file looking for something.
|
|
88
|
+
|
|
89
|
+
7. NON-INTERACTIVE COMMANDS ONLY. No vim, nano, interactive prompts. Use -y/--yes flags. 2 min timeout.
|
|
90
|
+
|
|
91
|
+
8. COMPLETE CODE. Never write "// ...", "// TODO", "// rest of code". Write the full implementation.
|
|
92
|
+
|
|
93
|
+
9. VERIFY. After changes: read back files, run tests, fix issues. Don't declare done without checking.
|
|
94
|
+
|
|
95
|
+
# Error Recovery
|
|
96
|
+
|
|
97
|
+
- edit_file "not found" → Read file again. Check whitespace. Content may have changed.
|
|
98
|
+
- edit_file "multiple matches" → Add more surrounding lines to old_string.
|
|
99
|
+
- Command failed → Read error. Check dependencies. Try different approach.
|
|
100
|
+
- File not found → Use glob/list_files to find correct path.
|
|
101
|
+
|
|
102
|
+
# Workflow: EXPLORE → PLAN (1-2 sentences) → EXECUTE → VERIFY
|
|
103
|
+
|
|
104
|
+
# Style: Direct. No filler. Brief explanations between tools. Summary when done.`;
|
|
105
|
+
|
|
106
|
+
} else {
|
|
107
|
+
prompt = `You are Vibe Hacker v${config.version} — expert AI for cybersecurity and engineering.
|
|
108
|
+
|
|
109
|
+
# Environment
|
|
110
|
+
- OS: ${os} | Date: ${date}
|
|
111
|
+
${projectSection}
|
|
112
|
+
# CHAT MODE — Expert Answers
|
|
113
|
+
|
|
114
|
+
Direct, accurate, actionable. No filler. Markdown with language-tagged code blocks.
|
|
115
|
+
For security: include attack vectors + mitigations. For code: working examples, not pseudocode.`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_promptCache = { mode, cwd, prompt };
|
|
119
|
+
return prompt;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Thinking Extraction ──────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
function extractThinking(text) {
|
|
125
|
+
let visible = text;
|
|
126
|
+
let thinking = '';
|
|
127
|
+
// Remove <think>...</think> and <thinking>...</thinking> blocks
|
|
128
|
+
visible = visible.replace(/<think(?:ing)?>([\s\S]*?)<\/think(?:ing)?>/g, (_, t) => {
|
|
129
|
+
thinking += t.trim() + '\n';
|
|
130
|
+
return '';
|
|
131
|
+
});
|
|
132
|
+
return { visible: visible.replace(/\n{3,}/g, '\n\n').trim(), thinking: thinking.trim() };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Token Estimation (fast, cached per string length) ────────────────────────
|
|
136
|
+
|
|
137
|
+
function estimateTokens(text) {
|
|
138
|
+
if (!text) return 0;
|
|
139
|
+
// Empirical: ~3.5 chars per token for mixed code/text
|
|
140
|
+
return Math.ceil(text.length / 3.5);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Context Trimming — Proactive, Prioritized ────────────────────────────────
|
|
144
|
+
|
|
145
|
+
function trimHistory(messages, maxContextTokens) {
|
|
146
|
+
// Fast total estimate
|
|
147
|
+
let total = 0;
|
|
148
|
+
for (let i = 0; i < messages.length; i++) total += estimateTokens(messages[i].content);
|
|
149
|
+
|
|
150
|
+
const budget = Math.floor(maxContextTokens * 0.55); // 45% headroom for response + tool results
|
|
151
|
+
if (total <= budget) return messages;
|
|
152
|
+
|
|
153
|
+
// Phase 1: Strip thinking blocks from old assistant messages
|
|
154
|
+
let trimmed = messages.map((m, i) => {
|
|
155
|
+
if (i === 0 || i >= messages.length - 6) return m; // keep system + recent
|
|
156
|
+
if (m.role === 'assistant' && (m.content.includes('<think>') || m.content.includes('<thinking>'))) {
|
|
157
|
+
const { visible } = extractThinking(m.content);
|
|
158
|
+
return { ...m, content: visible };
|
|
159
|
+
}
|
|
160
|
+
return m;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
total = 0;
|
|
164
|
+
for (const m of trimmed) total += estimateTokens(m.content);
|
|
165
|
+
if (total <= budget) return trimmed;
|
|
166
|
+
|
|
167
|
+
// Phase 2: Compress old tool results to headers only
|
|
168
|
+
trimmed = trimmed.map((m, i) => {
|
|
169
|
+
if (i === 0 || i >= trimmed.length - 6) return m;
|
|
170
|
+
if (m.role === 'user' && m.content.startsWith('[Tool Result:') && m.content.length > 800) {
|
|
171
|
+
return { ...m, content: m.content.split('\n')[0] + '\n[output trimmed]' };
|
|
172
|
+
}
|
|
173
|
+
if (m.role === 'assistant' && m.content.length > 1500 && i < trimmed.length - 8) {
|
|
174
|
+
return { ...m, content: m.content.substring(0, 400) + '\n[...]\n' + m.content.slice(-300) };
|
|
175
|
+
}
|
|
176
|
+
return m;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
total = 0;
|
|
180
|
+
for (const m of trimmed) total += estimateTokens(m.content);
|
|
181
|
+
if (total <= budget) return trimmed;
|
|
182
|
+
|
|
183
|
+
// Phase 3: Drop middle messages
|
|
184
|
+
const keep = Math.min(8, trimmed.length - 2);
|
|
185
|
+
if (trimmed.length <= keep + 2) return trimmed;
|
|
186
|
+
const dropped = trimmed.length - keep - 1;
|
|
187
|
+
return [
|
|
188
|
+
trimmed[0],
|
|
189
|
+
{ role: 'user', content: `[${dropped} earlier messages trimmed for context]` },
|
|
190
|
+
...trimmed.slice(-keep),
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Agent ────────────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
class Agent {
|
|
197
|
+
constructor() {
|
|
198
|
+
this.history = [];
|
|
199
|
+
this.mode = 'chat';
|
|
200
|
+
this.cwd = process.cwd();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
setMode(mode) { this.mode = mode; this.history = []; _promptCache.mode = null; }
|
|
204
|
+
setCwd(dir) { this.cwd = dir; _promptCache.cwd = null; _memCache.cwd = null; }
|
|
205
|
+
clearHistory(){ this.history = []; }
|
|
206
|
+
|
|
207
|
+
async run({ userMessage, model, signal, onToken, onDone, onError, onToolCall, onToolResult, beforeToolCall }) {
|
|
208
|
+
this.history.push({ role: 'user', content: userMessage });
|
|
209
|
+
|
|
210
|
+
const maxCtx = (model && model.contextWindow) || 32768;
|
|
211
|
+
const maxIter = config.maxToolIterations || 25;
|
|
212
|
+
let iterations = 0;
|
|
213
|
+
|
|
214
|
+
// ── Iterative agent loop ──────────────────────────────────────────
|
|
215
|
+
while (true) {
|
|
216
|
+
if (signal && signal.aborted) {
|
|
217
|
+
onError(Object.assign(new Error('aborted'), { type: 'ABORTED' }));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
iterations++;
|
|
222
|
+
if (iterations > maxIter) {
|
|
223
|
+
onDone('[Tool iteration limit reached. Use /retry to continue.]');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Build messages with proactive trimming (BEFORE sending)
|
|
228
|
+
const sysPrompt = buildSystemPrompt(this.mode, this.cwd);
|
|
229
|
+
let messages = [{ role: 'system', content: sysPrompt }, ...this.history];
|
|
230
|
+
messages = trimHistory(messages, maxCtx);
|
|
231
|
+
|
|
232
|
+
// ── Stream response ─────────────────────────────────────────────
|
|
233
|
+
let fullResponse = '';
|
|
234
|
+
let streamError = null;
|
|
235
|
+
|
|
236
|
+
await new Promise((resolve) => {
|
|
237
|
+
let resolved = false;
|
|
238
|
+
const done = () => { if (!resolved) { resolved = true; resolve(); } };
|
|
239
|
+
streamChat({
|
|
240
|
+
messages, model: model.id, signal,
|
|
241
|
+
maxTokens: model.maxTokens || config.maxTokens || 8192,
|
|
242
|
+
onToken: (token, full) => { fullResponse = full; if (onToken) onToken(token, full); },
|
|
243
|
+
onDone: (content) => { fullResponse = content || fullResponse; done(); },
|
|
244
|
+
onError: (errObj) => { streamError = errObj; done(); },
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Rate limit backoff inside tool loop — rotate-first strategy
|
|
249
|
+
if (streamError && streamError.type === 'RATE_LIMIT' && iterations > 1) {
|
|
250
|
+
// Don't retry same model. Surface error to app.js for provider rotation.
|
|
251
|
+
onError(Object.assign(new Error(streamError.msg || 'Rate limited'), streamError));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (streamError) {
|
|
255
|
+
onError(Object.assign(new Error(streamError.msg || 'error'), streamError));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (signal && signal.aborted) return;
|
|
259
|
+
|
|
260
|
+
// Store full response in history (including thinking for continuity)
|
|
261
|
+
this.history.push({ role: 'assistant', content: fullResponse });
|
|
262
|
+
|
|
263
|
+
// Chat mode — done after single response
|
|
264
|
+
if (this.mode !== 'hunt') {
|
|
265
|
+
const { visible } = extractThinking(fullResponse);
|
|
266
|
+
onDone(visible || fullResponse);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Hunt mode — parse tool calls
|
|
271
|
+
const toolCalls = parseToolCalls(fullResponse);
|
|
272
|
+
if (toolCalls.length === 0) {
|
|
273
|
+
const { visible } = extractThinking(fullResponse);
|
|
274
|
+
onDone(visible || fullResponse);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Execute all tools
|
|
279
|
+
for (const tc of toolCalls) {
|
|
280
|
+
if (signal && signal.aborted) return;
|
|
281
|
+
|
|
282
|
+
if (onToolCall) onToolCall(tc);
|
|
283
|
+
|
|
284
|
+
if (beforeToolCall) {
|
|
285
|
+
const decision = await beforeToolCall(tc);
|
|
286
|
+
if (decision === 'no') {
|
|
287
|
+
this.history.push({ role: 'user', content: `[Tool Denied: ${tc.name}] User rejected. Try a different approach.` });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let result;
|
|
293
|
+
const toolStart = Date.now();
|
|
294
|
+
try {
|
|
295
|
+
result = await executeTool(tc, this.cwd);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
result = `[Error: ${tc.name}] ${err.message}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (onToolResult) onToolResult(tc, result);
|
|
301
|
+
this.history.push({ role: 'user', content: `[Tool Result: ${tc.name}]\n${result}` });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Adaptive throttle based on tool types — minimal delay
|
|
305
|
+
const hasWrite = toolCalls.some(tc => ['write_file', 'edit_file', 'execute_command', 'delete_file'].includes(tc.name));
|
|
306
|
+
await new Promise(r => setTimeout(r, hasWrite ? 300 : 100));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = { Agent, MODES };
|