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/LICENSE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vibe Security
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
ADDITIONAL NOTICE — DUAL-USE SECURITY TOOL:
|
|
24
|
+
This software is a security research and penetration testing tool. It is
|
|
25
|
+
intended solely for authorized security testing on systems owned by the user
|
|
26
|
+
or for which the user has obtained explicit written permission to test. The
|
|
27
|
+
authors disclaim all liability for any unauthorized, illegal, or unethical
|
|
28
|
+
use. Users are solely responsible for ensuring compliance with all applicable
|
|
29
|
+
laws and for obtaining proper authorization before conducting any security
|
|
30
|
+
testing.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Vibe Hacker
|
|
2
|
+
|
|
3
|
+
> Terminal AI assistant for cybersecurity professionals. Free models, autonomous agent, multi-provider rotation. No credit card required.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/vibehacker)
|
|
6
|
+
[](https://github.com/agentichacker/vibehacker/blob/main/LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g vibehacker
|
|
13
|
+
vibehacker
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or zero-install:
|
|
17
|
+
```bash
|
|
18
|
+
npx vibehacker
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Get Started
|
|
22
|
+
|
|
23
|
+
1. Get a free API key at [vibsecurity.com](https://vibsecurity.com)
|
|
24
|
+
2. Paste your key when prompted
|
|
25
|
+
3. Start hacking
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **Chat Mode** — Security Q&A, threat intel, vulnerability research
|
|
30
|
+
- **Hunt Mode** — Autonomous agent with file system + shell access for pentesting, code review, exploit development
|
|
31
|
+
- **Multi-Provider** — Add keys for Groq, Gemini, Cerebras, Mistral, OpenAI, Anthropic, DeepSeek, xAI
|
|
32
|
+
- **Silent Auto-Rotation** — Rate limit on one model? Automatically switches to another. Zero downtime
|
|
33
|
+
- **Cross-Platform** — Windows, macOS, Linux, WSL
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
| Command | Description |
|
|
38
|
+
|---------|-------------|
|
|
39
|
+
| `/mode` | Switch between Chat and Hunt |
|
|
40
|
+
| `/addkey` | Add a provider API key |
|
|
41
|
+
| `/providers` | List configured providers |
|
|
42
|
+
| `/clear` | Clear chat and context |
|
|
43
|
+
| `/update` | Self-update to latest version |
|
|
44
|
+
| `/pro` | Upgrade to Pro (unlimited requests) |
|
|
45
|
+
| `/account` | View usage and tier info |
|
|
46
|
+
|
|
47
|
+
## Keyboard Shortcuts
|
|
48
|
+
|
|
49
|
+
| Key | Action |
|
|
50
|
+
|-----|--------|
|
|
51
|
+
| `Tab` | Cycle mode |
|
|
52
|
+
| `Ctrl+R` | Retry last message |
|
|
53
|
+
| `Ctrl+C` | Cancel / exit |
|
|
54
|
+
| `/` | Open command menu |
|
|
55
|
+
|
|
56
|
+
## Pricing
|
|
57
|
+
|
|
58
|
+
| | Free | Pro |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| Requests/day | 50 | Unlimited |
|
|
61
|
+
| Models | Free tier | All + frontier |
|
|
62
|
+
| Support | Community | Email |
|
|
63
|
+
| Price | $0 | $19/mo |
|
|
64
|
+
|
|
65
|
+
## Security
|
|
66
|
+
|
|
67
|
+
This is a dual-use security tool. Only use on systems you own or have written authorization to test. By using this tool you acknowledge that you are solely responsible for your actions.
|
|
68
|
+
|
|
69
|
+
## Links
|
|
70
|
+
|
|
71
|
+
- Website: [vibsecurity.com](https://vibsecurity.com)
|
|
72
|
+
- Issues: [github.com/agentichacker/vibehacker/issues](https://github.com/agentichacker/vibehacker/issues)
|
|
73
|
+
- Pricing: [vibsecurity.com/pricing](https://vibsecurity.com/pricing)
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT — see [LICENSE](LICENSE)
|
package/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WSL / terminal compatibility
|
|
5
|
+
if (!process.env.TERM || process.env.TERM === 'dumb') {
|
|
6
|
+
process.env.TERM = 'xterm-256color';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let _app = null;
|
|
10
|
+
|
|
11
|
+
process.on('SIGINT', () => { if (_app) _app._exit(); else { process.stdout.write('\r\n'); process.exit(0); } });
|
|
12
|
+
process.on('SIGTERM', () => { if (_app) _app._exit(); else process.exit(0); });
|
|
13
|
+
|
|
14
|
+
process.on('uncaughtException', (err) => {
|
|
15
|
+
process.stdout.write('\x1b[?25h\x1b[0m\r\n');
|
|
16
|
+
process.stderr.write(`\n\x1b[31m[Vibe Hacker Fatal]\x1b[0m ${err.message}\n${err.stack}\n`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
process.on('unhandledRejection', (reason) => {
|
|
21
|
+
process.stderr.write(`\nUnhandled rejection: ${reason}\n`);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const { HackerCLIApp } = require('./src/app');
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
_app = new HackerCLIApp();
|
|
28
|
+
try {
|
|
29
|
+
await _app.start();
|
|
30
|
+
} catch (err) {
|
|
31
|
+
process.stdout.write('\x1b[?25h\x1b[0m\r\n');
|
|
32
|
+
process.stderr.write(`\x1b[31m[Vibe Hacker]\x1b[0m Failed to start: ${err.message}\n`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibehacker",
|
|
3
|
+
"version": "4.1.0",
|
|
4
|
+
"description": "Vibe Hacker — Terminal AI cybersecurity assistant. Free models, autonomous agent, multi-provider rotation.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vibehacker": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"cybersecurity",
|
|
15
|
+
"hacking",
|
|
16
|
+
"terminal",
|
|
17
|
+
"cli",
|
|
18
|
+
"pentest",
|
|
19
|
+
"security",
|
|
20
|
+
"agent",
|
|
21
|
+
"autonomous",
|
|
22
|
+
"free",
|
|
23
|
+
"vibe",
|
|
24
|
+
"hacker",
|
|
25
|
+
"groq",
|
|
26
|
+
"gemini"
|
|
27
|
+
],
|
|
28
|
+
"author": "Vibe Security <hello@vibsecurity.com>",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/agentichacker/vibehacker.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/agentichacker/vibehacker/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://vibsecurity.com",
|
|
38
|
+
"files": [
|
|
39
|
+
"index.js",
|
|
40
|
+
"src/**",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"axios": "^1.6.7",
|
|
46
|
+
"blessed": "^0.1.81"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=16.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/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 };
|