veil-browser 0.1.4 → 0.1.6

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/dist/ai.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Page } from 'playwright';
2
+ import { humanDelay } from './browser.js';
2
3
  interface ActionStep {
3
4
  action: 'click' | 'type' | 'press' | 'navigate' | 'wait' | 'scroll' | 'select';
4
5
  selector?: string;
@@ -9,6 +10,14 @@ interface ActionStep {
9
10
  ms?: number;
10
11
  description?: string;
11
12
  }
13
+ interface LLMConfig {
14
+ provider: 'openai' | 'anthropic' | 'openrouter' | 'ollama';
15
+ apiKey?: string;
16
+ model: string;
17
+ baseUrl?: string;
18
+ }
19
+ declare function getActionsFromLLM(instruction: string, snapshot: string, pageUrl: string, llm: LLMConfig): Promise<ActionStep[]>;
20
+ declare function executeStep(page: Page, step: ActionStep): Promise<void>;
12
21
  export declare function aiAct(page: Page, instruction: string, opts?: {
13
22
  verbose?: boolean;
14
23
  }): Promise<{
@@ -16,4 +25,4 @@ export declare function aiAct(page: Page, instruction: string, opts?: {
16
25
  steps: ActionStep[];
17
26
  error?: string;
18
27
  }>;
19
- export {};
28
+ export { getActionsFromLLM, executeStep, humanDelay };
package/dist/ai.js CHANGED
@@ -228,6 +228,10 @@ Return JSON array of action steps:`;
228
228
  }
229
229
  // Execute a single action step with X-specific handling
230
230
  async function executeStep(page, step) {
231
+ // Validate before executing
232
+ if (step.action === 'navigate' && !step.url) {
233
+ throw new Error('navigate requires url parameter');
234
+ }
231
235
  switch (step.action) {
232
236
  case 'click': {
233
237
  if (!step.selector)
@@ -303,7 +307,18 @@ export async function aiAct(page, instruction, opts = {}) {
303
307
  const pageUrl = page.url();
304
308
  spinner.text = '🧠 Asking AI what to do...';
305
309
  // 2. Get action steps from LLM
306
- const steps = await getActionsFromLLM(instruction, snapshot, pageUrl, llm);
310
+ let steps = await getActionsFromLLM(instruction, snapshot, pageUrl, llm);
311
+ // 3. Filter out invalid steps (navigate without URL)
312
+ steps = steps.filter(s => {
313
+ if (s.action === 'navigate' && !s.url) {
314
+ console.warn(chalk.yellow('āš ļø Filtered out navigate action without URL'));
315
+ return false;
316
+ }
317
+ return true;
318
+ });
319
+ if (steps.length === 0) {
320
+ throw new Error('No valid action steps generated');
321
+ }
307
322
  if (opts.verbose) {
308
323
  spinner.stop();
309
324
  console.log(chalk.cyan('\nšŸ“‹ AI action plan:'));
@@ -332,3 +347,4 @@ export async function aiAct(page, instruction, opts = {}) {
332
347
  return { success: false, steps: [], error: err.message };
333
348
  }
334
349
  }
350
+ export { getActionsFromLLM, executeStep, humanDelay };
@@ -0,0 +1,16 @@
1
+ import { Page } from 'playwright';
2
+ export interface DaemonConfig {
3
+ instruction: string;
4
+ interval: number;
5
+ platform: 'x' | 'linkedin' | 'reddit' | 'bluesky';
6
+ maxRuns?: number;
7
+ stopOn?: string[];
8
+ }
9
+ /**
10
+ * Run veil in daemon mode — continuously execute actions at intervals
11
+ */
12
+ export declare function daemonCommand(config: DaemonConfig, executeAction: (page: Page, instruction: string) => Promise<void>): Promise<void>;
13
+ /**
14
+ * Parse daemon CLI args
15
+ */
16
+ export declare function parseDaemonArgs(args: Record<string, any>): DaemonConfig | null;
@@ -0,0 +1,124 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ /**
4
+ * Run veil in daemon mode — continuously execute actions at intervals
5
+ */
6
+ export async function daemonCommand(config, executeAction) {
7
+ const { instruction, interval, platform, maxRuns = Infinity, stopOn = [] } = config;
8
+ let runCount = 0;
9
+ let errors = 0;
10
+ const startTime = new Date();
11
+ console.log(chalk.cyan(`\nšŸ¤– veil daemon started`));
12
+ console.log(chalk.gray(` Instruction: ${instruction}`));
13
+ console.log(chalk.gray(` Interval: ${interval}m`));
14
+ console.log(chalk.gray(` Platform: ${platform}`));
15
+ console.log(chalk.gray(` Max runs: ${maxRuns === Infinity ? 'āˆž' : maxRuns}`));
16
+ console.log(chalk.gray(` Ctrl+C to stop\n`));
17
+ const intervalMs = interval * 60 * 1000;
18
+ // Graceful shutdown handler
19
+ const shutdown = (sig) => {
20
+ console.log(chalk.yellow(`\nā¹ļø ${sig} received, shutting down gracefully...`));
21
+ console.log(chalk.green(`\nāœ… Daemon stopped`));
22
+ console.log(chalk.gray(` Runs: ${runCount}`));
23
+ console.log(chalk.gray(` Errors: ${errors}`));
24
+ console.log(chalk.gray(` Duration: ${Math.round((Date.now() - startTime.getTime()) / 1000)}s`));
25
+ process.exit(0);
26
+ };
27
+ process.on('SIGINT', () => shutdown('SIGINT'));
28
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
29
+ // Run loop
30
+ const runLoop = async () => {
31
+ while (runCount < maxRuns) {
32
+ const spinner = ora({
33
+ text: `Run ${runCount + 1}/${maxRuns === Infinity ? 'āˆž' : maxRuns}`,
34
+ color: 'cyan',
35
+ }).start();
36
+ try {
37
+ // Import browser here to keep session fresh
38
+ const { chromium } = await import('playwright');
39
+ const browser = await chromium.launch({
40
+ headless: true,
41
+ args: ['--disable-blink-features=AutomationControlled'],
42
+ });
43
+ const context = await browser.newContext();
44
+ const page = await context.newPage();
45
+ // Navigate to platform home first
46
+ const platformUrls = {
47
+ x: 'https://x.com/home',
48
+ twitter: 'https://twitter.com/home',
49
+ linkedin: 'https://www.linkedin.com/feed',
50
+ reddit: 'https://www.reddit.com',
51
+ bluesky: 'https://bsky.app',
52
+ };
53
+ const startUrl = platformUrls[platform];
54
+ if (startUrl) {
55
+ await page.goto(startUrl, { waitUntil: 'domcontentloaded' }).catch(() => { });
56
+ // Wait for page to settle
57
+ await new Promise(r => setTimeout(r, 2000));
58
+ }
59
+ try {
60
+ await executeAction(page, instruction);
61
+ runCount++;
62
+ spinner.succeed(chalk.green(`āœ… Run ${runCount} succeeded`));
63
+ errors = 0; // Reset error counter on success
64
+ }
65
+ catch (err) {
66
+ errors++;
67
+ const errMsg = err.message ?? String(err);
68
+ if (stopOn.some(pattern => errMsg.includes(pattern))) {
69
+ spinner.fail(chalk.red(`Run ${runCount + 1} failed: ${errMsg}`));
70
+ console.log(chalk.red(`\nā›” Stop condition triggered: ${errMsg}`));
71
+ process.exit(1);
72
+ }
73
+ if (errors > 3) {
74
+ spinner.fail(chalk.red(`Run ${runCount + 1} failed (3rd error in a row)`));
75
+ console.log(chalk.red(`\nā›” Too many consecutive errors, stopping.`));
76
+ process.exit(1);
77
+ }
78
+ spinner.warn(chalk.yellow(`Run ${runCount + 1} failed: ${errMsg}`));
79
+ spinner.text = `Retrying in ${interval}m...`;
80
+ }
81
+ finally {
82
+ await page.close().catch(() => { });
83
+ await context.close().catch(() => { });
84
+ await browser.close().catch(() => { });
85
+ }
86
+ }
87
+ catch (fatalErr) {
88
+ spinner.fail(chalk.red(`Fatal error: ${fatalErr.message}`));
89
+ console.log(chalk.red(`\nā›” Daemon stopped due to fatal error`));
90
+ process.exit(1);
91
+ }
92
+ // Wait before next run
93
+ if (runCount < maxRuns) {
94
+ spinner.start(`Next run in ${interval}m...`);
95
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
96
+ spinner.stop();
97
+ }
98
+ }
99
+ // Reached max runs
100
+ console.log(chalk.green(`\nāœ… Completed ${maxRuns} runs`));
101
+ process.exit(0);
102
+ };
103
+ await runLoop();
104
+ }
105
+ /**
106
+ * Parse daemon CLI args
107
+ */
108
+ export function parseDaemonArgs(args) {
109
+ if (!args.daemon)
110
+ return null;
111
+ const instruction = args._?.[0];
112
+ if (!instruction) {
113
+ console.error(chalk.red('Error: instruction required'));
114
+ console.error(chalk.gray('Usage: veil daemon <instruction> --interval 5 --platform x [--max-runs 10]'));
115
+ process.exit(1);
116
+ }
117
+ return {
118
+ instruction,
119
+ interval: parseInt(args.interval ?? '5', 10),
120
+ platform: args.platform ?? 'x',
121
+ maxRuns: args.maxRuns ? parseInt(args.maxRuns, 10) : Infinity,
122
+ stopOn: args.stopOn ? (Array.isArray(args.stopOn) ? args.stopOn : [args.stopOn]) : [],
123
+ };
124
+ }
package/dist/index.js CHANGED
@@ -115,6 +115,36 @@ program
115
115
  await fs.writeFile(configFile, JSON.stringify(config, null, 2), 'utf-8');
116
116
  console.log(chalk.green(`āœ… Set ${key} = ${value}`));
117
117
  });
118
+ // --- Daemon mode ---
119
+ program
120
+ .command('daemon [instruction...]')
121
+ .description('Run veil continuously at intervals (like posts every 5 minutes)')
122
+ .option('--platform <platform>', 'Platform: x, linkedin, reddit, bluesky', 'x')
123
+ .option('--interval <minutes>', 'Run every N minutes', '5')
124
+ .option('--max-runs <n>', 'Stop after N runs (default: infinite)')
125
+ .option('--stop-on <errors...>', 'Stop if error contains this text')
126
+ .action(async (instructions, opts) => {
127
+ const instruction = instructions?.join(' ');
128
+ if (!instruction) {
129
+ console.error(chalk.red('Error: instruction required'));
130
+ console.error(chalk.gray('Usage: veil daemon "Like posts about AI" --interval 5 --platform x'));
131
+ process.exit(1);
132
+ }
133
+ const config = {
134
+ instruction,
135
+ interval: parseInt(opts.interval, 10),
136
+ platform: opts.platform,
137
+ maxRuns: opts.maxRuns ? parseInt(opts.maxRuns, 10) : Infinity,
138
+ stopOn: opts.stopOn || [],
139
+ };
140
+ const { daemonCommand } = await import('./commands/daemon.js');
141
+ const { aiAct } = await import('./ai.js');
142
+ await daemonCommand(config, async (page) => {
143
+ const result = await aiAct(page, config.instruction, {});
144
+ if (!result.success)
145
+ throw new Error(result.error);
146
+ });
147
+ });
118
148
  // --- MCP Server ---
119
149
  program
120
150
  .command('serve')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veil-browser",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Stealth browser CLI for AI agents — bypass bot detection, persist sessions, local CAPTCHA solving, MCP server",
5
5
  "keywords": [
6
6
  "browser",