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 +10 -1
- package/dist/ai.js +17 -1
- package/dist/commands/daemon.d.ts +16 -0
- package/dist/commands/daemon.js +124 -0
- package/dist/index.js +30 -0
- package/package.json +1 -1
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
|
-
|
|
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')
|