@stevederico/dotbot 0.19.0 → 0.20.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/CHANGELOG.md +12 -0
- package/README.md +3 -2
- package/bin/dotbot.js +8 -2
- package/core/browser-launcher.js +246 -0
- package/core/cdp.js +617 -0
- package/dotbot.db +0 -0
- package/index.js +0 -5
- package/package.json +4 -6
- package/storage/MemoryStore.js +1 -1
- package/storage/SQLiteAdapter.js +36 -1
- package/storage/index.js +2 -7
- package/tools/browser.js +479 -384
- package/storage/MongoAdapter.js +0 -291
- package/storage/MongoCronAdapter.js +0 -347
- package/storage/MongoTaskAdapter.js +0 -242
- package/storage/MongoTriggerAdapter.js +0 -158
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
<img src="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExaGNjeWoweGx4bGYxZXNvYmtsYW80MjlxODFmeTN0cHE3cHN6emFoNiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gYWeVOiMmbg3kzCTq5/giphy.gif" alt="dotbot" width="200">
|
|
3
3
|
<h1 align="center" style="border-bottom: none; margin-bottom: 0;">dotbot</h1>
|
|
4
4
|
<h3 align="center" style="margin-top: 0; font-weight: normal;">
|
|
5
|
-
The ultra-lean AI agent
|
|
5
|
+
The ultra-lean AI agent.<br>
|
|
6
|
+
11k lines. 47 tools. 0 dependencies.
|
|
6
7
|
</h3>
|
|
7
8
|
<p align="center">
|
|
8
9
|
<a href="https://opensource.org/licenses/mit">
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
<img src="https://img.shields.io/github/stars/stevederico/dotbot?style=social" alt="GitHub stars">
|
|
13
14
|
</a>
|
|
14
15
|
<a href="https://github.com/stevederico/dotbot">
|
|
15
|
-
<img src="https://img.shields.io/badge/version-0.
|
|
16
|
+
<img src="https://img.shields.io/badge/version-0.20-green" alt="version">
|
|
16
17
|
</a>
|
|
17
18
|
<img src="https://img.shields.io/badge/LOC-11k-orange" alt="Lines of Code">
|
|
18
19
|
</p>
|
package/bin/dotbot.js
CHANGED
|
@@ -22,9 +22,17 @@ process.emit = function (event, error) {
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { parseArgs } from 'node:util';
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { dirname, join } from 'node:path';
|
|
25
28
|
import * as readline from 'node:readline';
|
|
26
29
|
import { createServer } from 'node:http';
|
|
27
30
|
|
|
31
|
+
// Read version from package.json
|
|
32
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
34
|
+
const VERSION = pkg.version;
|
|
35
|
+
|
|
28
36
|
// Lazy-loaded modules (avoid SQLite import on --help)
|
|
29
37
|
let stores = null;
|
|
30
38
|
let coreTools = null;
|
|
@@ -49,8 +57,6 @@ async function loadModules() {
|
|
|
49
57
|
AI_PROVIDERS = mod.AI_PROVIDERS;
|
|
50
58
|
agentLoop = mod.agentLoop;
|
|
51
59
|
}
|
|
52
|
-
|
|
53
|
-
const VERSION = '0.19';
|
|
54
60
|
const DEFAULT_PORT = 3000;
|
|
55
61
|
const DEFAULT_DB = './dotbot.db';
|
|
56
62
|
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// core/browser-launcher.js
|
|
2
|
+
// Browser launcher and downloader for CDP automation.
|
|
3
|
+
// Downloads Chrome for Testing on first use, zero npm dependencies.
|
|
4
|
+
|
|
5
|
+
import { spawn, execSync } from 'node:child_process';
|
|
6
|
+
import { existsSync, mkdirSync, createWriteStream, rmSync, chmodSync, readdirSync } from 'node:fs';
|
|
7
|
+
import { homedir, platform } from 'node:os';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { pipeline } from 'node:stream/promises';
|
|
10
|
+
import { createGunzip } from 'node:zlib';
|
|
11
|
+
|
|
12
|
+
// Chrome for Testing version (stable channel)
|
|
13
|
+
const CHROME_VERSION = '131.0.6778.204';
|
|
14
|
+
|
|
15
|
+
const DOWNLOAD_URLS = {
|
|
16
|
+
darwin: `https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/mac-x64/chrome-headless-shell-mac-x64.zip`,
|
|
17
|
+
'darwin-arm64': `https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/mac-arm64/chrome-headless-shell-mac-arm64.zip`,
|
|
18
|
+
linux: `https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chrome-headless-shell-linux64.zip`,
|
|
19
|
+
win32: `https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/win64/chrome-headless-shell-win64.zip`
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const BROWSER_DIR = join(homedir(), '.dotbot', 'browsers');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the expected Chrome executable path for this platform.
|
|
26
|
+
* @returns {string} Path to Chrome executable
|
|
27
|
+
*/
|
|
28
|
+
function getChromePath() {
|
|
29
|
+
const plat = platform();
|
|
30
|
+
const arch = process.arch;
|
|
31
|
+
|
|
32
|
+
if (plat === 'darwin') {
|
|
33
|
+
const archDir = arch === 'arm64' ? 'chrome-headless-shell-mac-arm64' : 'chrome-headless-shell-mac-x64';
|
|
34
|
+
return join(BROWSER_DIR, archDir, 'chrome-headless-shell');
|
|
35
|
+
} else if (plat === 'linux') {
|
|
36
|
+
return join(BROWSER_DIR, 'chrome-headless-shell-linux64', 'chrome-headless-shell');
|
|
37
|
+
} else if (plat === 'win32') {
|
|
38
|
+
return join(BROWSER_DIR, 'chrome-headless-shell-win64', 'chrome-headless-shell.exe');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw new Error(`Unsupported platform: ${plat}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the download URL for this platform.
|
|
46
|
+
* @returns {string} Download URL
|
|
47
|
+
*/
|
|
48
|
+
function getDownloadUrl() {
|
|
49
|
+
const plat = platform();
|
|
50
|
+
const arch = process.arch;
|
|
51
|
+
|
|
52
|
+
if (plat === 'darwin' && arch === 'arm64') {
|
|
53
|
+
return DOWNLOAD_URLS['darwin-arm64'];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return DOWNLOAD_URLS[plat];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Download and extract Chrome for Testing.
|
|
61
|
+
* @returns {Promise<string>} Path to Chrome executable
|
|
62
|
+
*/
|
|
63
|
+
async function downloadChrome() {
|
|
64
|
+
const url = getDownloadUrl();
|
|
65
|
+
if (!url) {
|
|
66
|
+
throw new Error(`No Chrome download available for ${platform()}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log('[browser] Downloading Chrome for Testing (~50MB)...');
|
|
70
|
+
|
|
71
|
+
mkdirSync(BROWSER_DIR, { recursive: true });
|
|
72
|
+
|
|
73
|
+
const zipPath = join(BROWSER_DIR, 'chrome.zip');
|
|
74
|
+
|
|
75
|
+
// Download zip file
|
|
76
|
+
const response = await fetch(url);
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(`Failed to download Chrome: ${response.status}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const fileStream = createWriteStream(zipPath);
|
|
82
|
+
await pipeline(response.body, fileStream);
|
|
83
|
+
|
|
84
|
+
console.log('[browser] Extracting Chrome...');
|
|
85
|
+
|
|
86
|
+
// Extract using system unzip (available on all platforms)
|
|
87
|
+
try {
|
|
88
|
+
execSync(`unzip -o -q "${zipPath}" -d "${BROWSER_DIR}"`, { stdio: 'pipe' });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
throw new Error(`Failed to extract Chrome: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Clean up zip file
|
|
94
|
+
rmSync(zipPath, { force: true });
|
|
95
|
+
|
|
96
|
+
const chromePath = getChromePath();
|
|
97
|
+
|
|
98
|
+
// Make executable on Unix
|
|
99
|
+
if (platform() !== 'win32') {
|
|
100
|
+
chmodSync(chromePath, 0o755);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log('[browser] Chrome installed successfully');
|
|
104
|
+
return chromePath;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Ensure Chrome is available, downloading if necessary.
|
|
109
|
+
* @returns {Promise<string>} Path to Chrome executable
|
|
110
|
+
*/
|
|
111
|
+
export async function ensureBrowser() {
|
|
112
|
+
const chromePath = getChromePath();
|
|
113
|
+
|
|
114
|
+
if (existsSync(chromePath)) {
|
|
115
|
+
return chromePath;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return downloadChrome();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Find a free port for Chrome debugging.
|
|
123
|
+
* @returns {Promise<number>} Available port number
|
|
124
|
+
*/
|
|
125
|
+
async function findFreePort() {
|
|
126
|
+
const { createServer } = await import('node:net');
|
|
127
|
+
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const server = createServer();
|
|
130
|
+
server.listen(0, () => {
|
|
131
|
+
const port = server.address().port;
|
|
132
|
+
server.close(() => resolve(port));
|
|
133
|
+
});
|
|
134
|
+
server.on('error', reject);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Launch Chrome with remote debugging enabled.
|
|
140
|
+
* @param {Object} options - Launch options
|
|
141
|
+
* @param {number} options.port - Debugging port (auto-assigned if not specified)
|
|
142
|
+
* @param {string} options.userDataDir - User data directory for isolation
|
|
143
|
+
* @returns {Promise<{process: ChildProcess, port: number, wsUrl: string}>}
|
|
144
|
+
*/
|
|
145
|
+
export async function launchBrowser(options = {}) {
|
|
146
|
+
const chromePath = await ensureBrowser();
|
|
147
|
+
const port = options.port || await findFreePort();
|
|
148
|
+
const userDataDir = options.userDataDir || `/tmp/dotbot-browser-${Date.now()}`;
|
|
149
|
+
|
|
150
|
+
mkdirSync(userDataDir, { recursive: true });
|
|
151
|
+
|
|
152
|
+
const args = [
|
|
153
|
+
`--remote-debugging-port=${port}`,
|
|
154
|
+
`--user-data-dir=${userDataDir}`,
|
|
155
|
+
'--disable-gpu',
|
|
156
|
+
'--no-sandbox',
|
|
157
|
+
'--disable-setuid-sandbox',
|
|
158
|
+
'--disable-dev-shm-usage',
|
|
159
|
+
'--disable-extensions',
|
|
160
|
+
'--disable-background-networking',
|
|
161
|
+
'--disable-sync',
|
|
162
|
+
'--no-first-run',
|
|
163
|
+
'--disable-default-apps'
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const proc = spawn(chromePath, args, {
|
|
167
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
168
|
+
detached: false
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Wait for DevTools to be ready
|
|
172
|
+
const wsUrl = await waitForDevTools(port, 10000);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
process: proc,
|
|
176
|
+
port,
|
|
177
|
+
wsUrl,
|
|
178
|
+
userDataDir
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Wait for Chrome DevTools to be ready and return WebSocket URL.
|
|
184
|
+
* @param {number} port - Debugging port
|
|
185
|
+
* @param {number} timeout - Timeout in ms
|
|
186
|
+
* @returns {Promise<string>} WebSocket debugger URL
|
|
187
|
+
*/
|
|
188
|
+
async function waitForDevTools(port, timeout = 10000) {
|
|
189
|
+
const start = Date.now();
|
|
190
|
+
const url = `http://127.0.0.1:${port}/json/version`;
|
|
191
|
+
|
|
192
|
+
while (Date.now() - start < timeout) {
|
|
193
|
+
try {
|
|
194
|
+
const response = await fetch(url);
|
|
195
|
+
if (response.ok) {
|
|
196
|
+
const data = await response.json();
|
|
197
|
+
return data.webSocketDebuggerUrl;
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Chrome not ready yet
|
|
201
|
+
}
|
|
202
|
+
await new Promise(r => setTimeout(r, 100));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
throw new Error('Chrome DevTools did not start in time');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a new browser context (tab) via CDP.
|
|
210
|
+
* @param {string} wsUrl - Browser WebSocket URL
|
|
211
|
+
* @returns {Promise<string>} Target WebSocket URL for the new context
|
|
212
|
+
*/
|
|
213
|
+
export async function createBrowserContext(wsUrl) {
|
|
214
|
+
// Connect to browser endpoint to create new target
|
|
215
|
+
const port = new URL(wsUrl).port;
|
|
216
|
+
const response = await fetch(`http://127.0.0.1:${port}/json/new`);
|
|
217
|
+
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
throw new Error('Failed to create browser context');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const target = await response.json();
|
|
223
|
+
return target.webSocketDebuggerUrl;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Close a browser context (tab) via CDP.
|
|
228
|
+
* @param {string} targetWsUrl - Target WebSocket URL
|
|
229
|
+
*/
|
|
230
|
+
export async function closeBrowserContext(targetWsUrl) {
|
|
231
|
+
const url = new URL(targetWsUrl);
|
|
232
|
+
const port = url.port;
|
|
233
|
+
const targetId = url.pathname.split('/').pop();
|
|
234
|
+
|
|
235
|
+
await fetch(`http://127.0.0.1:${port}/json/close/${targetId}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Kill the browser process.
|
|
240
|
+
* @param {ChildProcess} proc - Browser process
|
|
241
|
+
*/
|
|
242
|
+
export function killBrowser(proc) {
|
|
243
|
+
if (proc && !proc.killed) {
|
|
244
|
+
proc.kill('SIGTERM');
|
|
245
|
+
}
|
|
246
|
+
}
|