veil-browser 0.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/README.md +146 -0
- package/dist/ai.d.ts +19 -0
- package/dist/ai.js +253 -0
- package/dist/browser.d.ts +12 -0
- package/dist/browser.js +46 -0
- package/dist/captcha.d.ts +27 -0
- package/dist/captcha.js +203 -0
- package/dist/commands/act.d.ts +7 -0
- package/dist/commands/act.js +107 -0
- package/dist/commands/extract.d.ts +6 -0
- package/dist/commands/extract.js +64 -0
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +63 -0
- package/dist/commands/navigate.d.ts +5 -0
- package/dist/commands/navigate.js +23 -0
- package/dist/commands/screenshot.d.ts +7 -0
- package/dist/commands/screenshot.js +27 -0
- package/dist/commands/search.d.ts +13 -0
- package/dist/commands/search.js +196 -0
- package/dist/commands/session.d.ts +2 -0
- package/dist/commands/session.js +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +123 -0
- package/dist/local-captcha.d.ts +17 -0
- package/dist/local-captcha.js +285 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +232 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +37 -0
- package/dist/startup.d.ts +5 -0
- package/dist/startup.js +9 -0
- package/dist/state.d.ts +13 -0
- package/dist/state.js +45 -0
- package/package.json +54 -0
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { getBrowser, closeBrowser, humanDelay } from './browser.js';
|
|
3
|
+
import { searchCommand } from './commands/search.js';
|
|
4
|
+
import { listSessions, loadSession } from './session.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
const TOOLS = [
|
|
7
|
+
{
|
|
8
|
+
name: 'veil_navigate',
|
|
9
|
+
description: 'Navigate to a URL in stealth headless mode using a saved session',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
url: { type: 'string', description: 'URL to navigate to' },
|
|
14
|
+
platform: { type: 'string', description: 'Platform session to use (e.g. twitter, reddit)' },
|
|
15
|
+
headed: { type: 'boolean', description: 'Run in visible browser mode' },
|
|
16
|
+
},
|
|
17
|
+
required: ['url'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'veil_act',
|
|
22
|
+
description: 'Perform a natural language action in the browser (click, type, post tweet, scroll, etc)',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
instruction: { type: 'string', description: 'Natural language instruction, e.g. "post tweet: hello world"' },
|
|
27
|
+
platform: { type: 'string', description: 'Platform session to use' },
|
|
28
|
+
url: { type: 'string', description: 'Navigate to this URL first' },
|
|
29
|
+
headed: { type: 'boolean' },
|
|
30
|
+
},
|
|
31
|
+
required: ['instruction'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'veil_search',
|
|
36
|
+
description: 'Search the web using Google, Bing, DuckDuckGo, or Twitter. Returns JSON results.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
query: { type: 'string', description: 'Search query' },
|
|
41
|
+
engine: { type: 'string', description: 'Comma-separated engines: google,bing,duckduckgo,twitter', default: 'google' },
|
|
42
|
+
limit: { type: 'number', description: 'Max results', default: 10 },
|
|
43
|
+
},
|
|
44
|
+
required: ['query'],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'veil_extract',
|
|
49
|
+
description: 'Extract data from a web page. Supports: tweets, links, text, title, metadata.',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
query: { type: 'string', description: 'What to extract: tweets, links, text, title' },
|
|
54
|
+
url: { type: 'string', description: 'URL to extract from' },
|
|
55
|
+
platform: { type: 'string', description: 'Platform session to use' },
|
|
56
|
+
},
|
|
57
|
+
required: ['query'],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'veil_screenshot',
|
|
62
|
+
description: 'Take a screenshot of a page. Returns the file path.',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
url: { type: 'string' },
|
|
67
|
+
platform: { type: 'string' },
|
|
68
|
+
output: { type: 'string', description: 'Output file path' },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'veil_session_list',
|
|
74
|
+
description: 'List all saved platform sessions',
|
|
75
|
+
inputSchema: { type: 'object', properties: {} },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'veil_login_status',
|
|
79
|
+
description: 'Check if a session exists for a given platform',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
platform: { type: 'string' },
|
|
84
|
+
},
|
|
85
|
+
required: ['platform'],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
async function callTool(name, args) {
|
|
90
|
+
switch (name) {
|
|
91
|
+
case 'veil_navigate': {
|
|
92
|
+
const { page } = await getBrowser({ headed: args.headed, platform: args.platform });
|
|
93
|
+
await page.goto(args.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
94
|
+
await humanDelay(500, 1200);
|
|
95
|
+
const title = await page.title();
|
|
96
|
+
const url = page.url();
|
|
97
|
+
await closeBrowser(args.platform);
|
|
98
|
+
return { success: true, url, title };
|
|
99
|
+
}
|
|
100
|
+
case 'veil_act': {
|
|
101
|
+
const { actCommand } = await import('./commands/act.js');
|
|
102
|
+
// Capture stdout
|
|
103
|
+
const chunks = [];
|
|
104
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
105
|
+
process.stdout.write = (c) => chunks.push(c.toString());
|
|
106
|
+
await actCommand(args.instruction, { platform: args.platform, url: args.url, headed: args.headed, json: true });
|
|
107
|
+
process.stdout.write = orig;
|
|
108
|
+
const out = chunks.join('');
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(out.split('\n').filter(Boolean).pop() ?? '{}');
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return { success: true };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
case 'veil_search': {
|
|
117
|
+
const chunks = [];
|
|
118
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
119
|
+
process.stdout.write = (c) => chunks.push(c.toString());
|
|
120
|
+
await searchCommand(args.query, { engine: args.engine ?? 'google', json: true, limit: String(args.limit ?? 10) });
|
|
121
|
+
process.stdout.write = orig;
|
|
122
|
+
const out = chunks.join('');
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(out.split('\n').filter(Boolean).pop() ?? '{}');
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return { success: false };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
case 'veil_extract': {
|
|
131
|
+
const { extractCommand } = await import('./commands/extract.js');
|
|
132
|
+
const chunks = [];
|
|
133
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
134
|
+
process.stdout.write = (c) => chunks.push(c.toString());
|
|
135
|
+
await extractCommand(args.query, { platform: args.platform, url: args.url, json: true });
|
|
136
|
+
process.stdout.write = orig;
|
|
137
|
+
const out = chunks.join('');
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(out.split('\n').filter(Boolean).pop() ?? '{}');
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return { success: false };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
case 'veil_screenshot': {
|
|
146
|
+
const { screenshotCommand } = await import('./commands/screenshot.js');
|
|
147
|
+
const chunks = [];
|
|
148
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
149
|
+
process.stdout.write = (c) => chunks.push(c.toString());
|
|
150
|
+
await screenshotCommand({ platform: args.platform, url: args.url, output: args.output, json: true });
|
|
151
|
+
process.stdout.write = orig;
|
|
152
|
+
const out = chunks.join('');
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(out.split('\n').filter(Boolean).pop() ?? '{}');
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return { success: false };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
case 'veil_session_list': {
|
|
161
|
+
const sessions = await listSessions();
|
|
162
|
+
return { sessions };
|
|
163
|
+
}
|
|
164
|
+
case 'veil_login_status': {
|
|
165
|
+
const session = await loadSession(args.platform);
|
|
166
|
+
return { platform: args.platform, loggedIn: !!session };
|
|
167
|
+
}
|
|
168
|
+
default:
|
|
169
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function jsonRpc(id, result, error) {
|
|
173
|
+
if (error)
|
|
174
|
+
return { jsonrpc: '2.0', id, error: { code: -32000, message: error } };
|
|
175
|
+
return { jsonrpc: '2.0', id, result };
|
|
176
|
+
}
|
|
177
|
+
async function handleRequest(body) {
|
|
178
|
+
const { id, method, params } = body;
|
|
179
|
+
if (method === 'initialize') {
|
|
180
|
+
return jsonRpc(id, {
|
|
181
|
+
protocolVersion: '2024-11-05',
|
|
182
|
+
capabilities: { tools: {} },
|
|
183
|
+
serverInfo: { name: 'veil', version: '0.1.0' },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (method === 'tools/list') {
|
|
187
|
+
return jsonRpc(id, { tools: TOOLS });
|
|
188
|
+
}
|
|
189
|
+
if (method === 'tools/call') {
|
|
190
|
+
try {
|
|
191
|
+
const result = await callTool(params.name, params.arguments ?? {});
|
|
192
|
+
return jsonRpc(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
return jsonRpc(id, undefined, err.message);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return jsonRpc(id, undefined, `Method not found: ${method}`);
|
|
199
|
+
}
|
|
200
|
+
export async function startMcpServer(port = 3456) {
|
|
201
|
+
const server = createServer(async (req, res) => {
|
|
202
|
+
if (req.method === 'POST') {
|
|
203
|
+
let body = '';
|
|
204
|
+
req.on('data', (chunk) => (body += chunk));
|
|
205
|
+
req.on('end', async () => {
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(body);
|
|
208
|
+
const response = await handleRequest(parsed);
|
|
209
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
210
|
+
res.end(JSON.stringify(response));
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
214
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
else if (req.method === 'GET' && req.url === '/health') {
|
|
219
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
220
|
+
res.end(JSON.stringify({ status: 'ok', version: '0.1.0' }));
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
res.writeHead(404);
|
|
224
|
+
res.end();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
server.listen(port, () => {
|
|
228
|
+
console.log(chalk.cyan(`\n🕶️ veil MCP server running on http://localhost:${port}`));
|
|
229
|
+
console.log(chalk.gray(` Tools: ${TOOLS.map((t) => t.name).join(', ')}\n`));
|
|
230
|
+
});
|
|
231
|
+
process.on('SIGINT', () => { server.close(); process.exit(0); });
|
|
232
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function saveSession(platform: string, state: object): Promise<void>;
|
|
2
|
+
export declare function loadSession(platform: string): Promise<any | null>;
|
|
3
|
+
export declare function deleteSession(platform: string): Promise<boolean>;
|
|
4
|
+
export declare function listSessions(): Promise<string[]>;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
const SESSION_DIR = join(homedir(), '.veil', 'sessions');
|
|
5
|
+
async function ensureDir() {
|
|
6
|
+
await fs.mkdir(SESSION_DIR, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
export async function saveSession(platform, state) {
|
|
9
|
+
await ensureDir();
|
|
10
|
+
const file = join(SESSION_DIR, `${platform}.json`);
|
|
11
|
+
await fs.writeFile(file, JSON.stringify(state, null, 2), 'utf-8');
|
|
12
|
+
}
|
|
13
|
+
export async function loadSession(platform) {
|
|
14
|
+
const file = join(SESSION_DIR, `${platform}.json`);
|
|
15
|
+
try {
|
|
16
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function deleteSession(platform) {
|
|
24
|
+
const file = join(SESSION_DIR, `${platform}.json`);
|
|
25
|
+
try {
|
|
26
|
+
await fs.unlink(file);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function listSessions() {
|
|
34
|
+
await ensureDir();
|
|
35
|
+
const files = await fs.readdir(SESSION_DIR);
|
|
36
|
+
return files.filter((f) => f.endsWith('.json')).map((f) => f.replace('.json', ''));
|
|
37
|
+
}
|
package/dist/startup.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ensureFlareSolverr } from './local-captcha.js';
|
|
2
|
+
/**
|
|
3
|
+
* Called once when veil starts any browser command.
|
|
4
|
+
* Boots FlareSolverr in the background so it's ready before we hit a CAPTCHA.
|
|
5
|
+
*/
|
|
6
|
+
export async function veilStartup() {
|
|
7
|
+
// Start FlareSolverr silently in background — don't await, don't block
|
|
8
|
+
ensureFlareSolverr().catch(() => { });
|
|
9
|
+
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface VeilState {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
currentUrl: string;
|
|
4
|
+
platform?: string;
|
|
5
|
+
pageTitle?: string;
|
|
6
|
+
lastAction: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
history: string[];
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function saveState(sessionId: string, state: Partial<VeilState>): Promise<void>;
|
|
12
|
+
export declare function loadState(sessionId: string): Promise<VeilState | null>;
|
|
13
|
+
export declare function clearState(sessionId: string): Promise<void>;
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
const STATE_DIR = join(homedir(), '.veil', 'state');
|
|
5
|
+
async function ensureDir() {
|
|
6
|
+
await fs.mkdir(STATE_DIR, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
export async function saveState(sessionId, state) {
|
|
9
|
+
await ensureDir();
|
|
10
|
+
const file = join(STATE_DIR, `${sessionId}.json`);
|
|
11
|
+
let existing = {
|
|
12
|
+
sessionId,
|
|
13
|
+
currentUrl: '',
|
|
14
|
+
lastAction: '',
|
|
15
|
+
timestamp: Date.now(),
|
|
16
|
+
history: [],
|
|
17
|
+
};
|
|
18
|
+
try {
|
|
19
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
20
|
+
existing = JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
const updated = { ...existing, ...state, timestamp: Date.now() };
|
|
24
|
+
if (state.currentUrl && state.currentUrl !== existing.currentUrl) {
|
|
25
|
+
updated.history = [...(existing.history ?? []).slice(-19), state.currentUrl];
|
|
26
|
+
}
|
|
27
|
+
await fs.writeFile(file, JSON.stringify(updated, null, 2), 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
export async function loadState(sessionId) {
|
|
30
|
+
const file = join(STATE_DIR, `${sessionId}.json`);
|
|
31
|
+
try {
|
|
32
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function clearState(sessionId) {
|
|
40
|
+
const file = join(STATE_DIR, `${sessionId}.json`);
|
|
41
|
+
try {
|
|
42
|
+
await fs.unlink(file);
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "veil-browser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stealth browser CLI for AI agents — bypass bot detection, persist sessions, local CAPTCHA solving, MCP server",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"browser",
|
|
7
|
+
"automation",
|
|
8
|
+
"stealth",
|
|
9
|
+
"playwright",
|
|
10
|
+
"cli",
|
|
11
|
+
"captcha",
|
|
12
|
+
"ai-agent",
|
|
13
|
+
"mcp"
|
|
14
|
+
],
|
|
15
|
+
"author": "CUTTLELAB",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"homepage": "https://github.com/cuttlelab/veil",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/cuttlelab/veil.git"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"veil": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"dev": "ts-node src/index.ts",
|
|
29
|
+
"start": "node dist/index.js",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist/**/*",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"chalk": "^5.3.0",
|
|
38
|
+
"commander": "^12.1.0",
|
|
39
|
+
"ora": "^8.1.1",
|
|
40
|
+
"playwright": "^1.50.0",
|
|
41
|
+
"playwright-extra": "^4.3.6",
|
|
42
|
+
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
43
|
+
"tesseract.js": "^5.1.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.0.0",
|
|
47
|
+
"ts-node": "^10.9.2",
|
|
48
|
+
"typescript": "^5.4.5"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18.0.0"
|
|
52
|
+
},
|
|
53
|
+
"type": "module"
|
|
54
|
+
}
|