agent-browser 0.0.0 → 0.1.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.
Files changed (54) hide show
  1. package/.prettierrc +7 -0
  2. package/LICENSE +201 -0
  3. package/README.md +274 -1
  4. package/bin/agent-browser +2 -0
  5. package/dist/actions.d.ts +7 -0
  6. package/dist/actions.d.ts.map +1 -0
  7. package/dist/actions.js +1138 -0
  8. package/dist/actions.js.map +1 -0
  9. package/dist/browser.d.ts +232 -0
  10. package/dist/browser.d.ts.map +1 -0
  11. package/dist/browser.js +477 -0
  12. package/dist/browser.js.map +1 -0
  13. package/dist/browser.test.d.ts +2 -0
  14. package/dist/browser.test.d.ts.map +1 -0
  15. package/dist/browser.test.js +136 -0
  16. package/dist/browser.test.js.map +1 -0
  17. package/dist/client.d.ts +17 -0
  18. package/dist/client.d.ts.map +1 -0
  19. package/dist/client.js +133 -0
  20. package/dist/client.js.map +1 -0
  21. package/dist/daemon.d.ts +29 -0
  22. package/dist/daemon.d.ts.map +1 -0
  23. package/dist/daemon.js +165 -0
  24. package/dist/daemon.js.map +1 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +1158 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/protocol.d.ts +26 -0
  30. package/dist/protocol.d.ts.map +1 -0
  31. package/dist/protocol.js +717 -0
  32. package/dist/protocol.js.map +1 -0
  33. package/dist/protocol.test.d.ts +2 -0
  34. package/dist/protocol.test.d.ts.map +1 -0
  35. package/dist/protocol.test.js +176 -0
  36. package/dist/protocol.test.js.map +1 -0
  37. package/dist/types.d.ts +604 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/types.js +2 -0
  40. package/dist/types.js.map +1 -0
  41. package/package.json +39 -8
  42. package/scripts/postinstall.js +17 -0
  43. package/src/actions.ts +1658 -0
  44. package/src/browser.test.ts +157 -0
  45. package/src/browser.ts +586 -0
  46. package/src/client.ts +150 -0
  47. package/src/daemon.ts +187 -0
  48. package/src/index.ts +1180 -0
  49. package/src/protocol.test.ts +216 -0
  50. package/src/protocol.ts +848 -0
  51. package/src/types.ts +913 -0
  52. package/tsconfig.json +19 -0
  53. package/vitest.config.ts +9 -0
  54. package/index.js +0 -2
package/src/client.ts ADDED
@@ -0,0 +1,150 @@
1
+ import * as net from 'net';
2
+ import { spawn } from 'child_process';
3
+ import { fileURLToPath } from 'url';
4
+ import * as path from 'path';
5
+ import * as fs from 'fs';
6
+ import { getSocketPath, isDaemonRunning, setSession, getSession } from './daemon.js';
7
+ import type { Response } from './types.js';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ let DEBUG = false;
12
+
13
+ export function setDebug(enabled: boolean): void {
14
+ DEBUG = enabled;
15
+ }
16
+
17
+ export { setSession, getSession };
18
+
19
+ function debug(...args: unknown[]): void {
20
+ if (DEBUG) {
21
+ console.error('[debug]', ...args);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Wait for socket to exist
27
+ */
28
+ async function waitForSocket(maxAttempts = 30): Promise<boolean> {
29
+ const socketPath = getSocketPath();
30
+ debug('Waiting for socket at', socketPath);
31
+ for (let i = 0; i < maxAttempts; i++) {
32
+ if (fs.existsSync(socketPath)) {
33
+ debug('Socket found after', i * 100, 'ms');
34
+ return true;
35
+ }
36
+ await new Promise((r) => setTimeout(r, 100));
37
+ }
38
+ debug('Socket not found after', maxAttempts * 100, 'ms');
39
+ return false;
40
+ }
41
+
42
+ /**
43
+ * Ensure daemon is running, start if not
44
+ */
45
+ export async function ensureDaemon(): Promise<void> {
46
+ const session = getSession();
47
+ debug(`Checking if daemon is running for session "${session}"...`);
48
+ if (isDaemonRunning()) {
49
+ debug('Daemon already running');
50
+ return;
51
+ }
52
+
53
+ debug('Starting daemon...');
54
+ const daemonPath = path.join(__dirname, 'daemon.js');
55
+ const child = spawn(process.execPath, [daemonPath], {
56
+ detached: true,
57
+ stdio: 'ignore',
58
+ env: { ...process.env, AGENT_BROWSER_DAEMON: '1', AGENT_BROWSER_SESSION: session },
59
+ });
60
+ child.unref();
61
+
62
+ // Wait for socket to be created
63
+ const ready = await waitForSocket();
64
+ if (!ready) {
65
+ throw new Error('Failed to start daemon');
66
+ }
67
+
68
+ debug(`Daemon started for session "${session}"`);
69
+ }
70
+
71
+ /**
72
+ * Send a command to the daemon
73
+ */
74
+ export async function sendCommand(command: Record<string, unknown>): Promise<Response> {
75
+ const socketPath = getSocketPath();
76
+ debug('Sending command:', JSON.stringify(command));
77
+
78
+ return new Promise((resolve, reject) => {
79
+ let resolved = false;
80
+ let buffer = '';
81
+ const startTime = Date.now();
82
+
83
+ const socket = net.createConnection(socketPath);
84
+
85
+ socket.on('connect', () => {
86
+ debug('Connected to daemon, sending command...');
87
+ socket.write(JSON.stringify(command) + '\n');
88
+ });
89
+
90
+ socket.on('data', (data) => {
91
+ buffer += data.toString();
92
+ debug('Received data:', buffer.length, 'bytes');
93
+
94
+ // Try to parse complete JSON from buffer
95
+ const newlineIdx = buffer.indexOf('\n');
96
+ if (newlineIdx !== -1) {
97
+ const jsonStr = buffer.substring(0, newlineIdx);
98
+ try {
99
+ const response = JSON.parse(jsonStr) as Response;
100
+ debug('Response received in', Date.now() - startTime, 'ms');
101
+ resolved = true;
102
+ socket.end();
103
+ resolve(response);
104
+ } catch (e) {
105
+ debug('JSON parse error:', e);
106
+ }
107
+ }
108
+ });
109
+
110
+ socket.on('error', (err) => {
111
+ debug('Socket error:', err.message);
112
+ if (!resolved) {
113
+ reject(new Error(`Connection error: ${err.message}`));
114
+ }
115
+ });
116
+
117
+ socket.on('close', () => {
118
+ debug('Socket closed, resolved:', resolved, 'buffer:', buffer.length);
119
+ if (!resolved && buffer.trim()) {
120
+ try {
121
+ const response = JSON.parse(buffer.trim()) as Response;
122
+ resolve(response);
123
+ } catch {
124
+ reject(new Error('Invalid response from daemon'));
125
+ }
126
+ } else if (!resolved) {
127
+ reject(new Error('Connection closed without response'));
128
+ }
129
+ });
130
+
131
+ // Timeout after 15 seconds (allows for 10s Playwright timeout + overhead)
132
+ setTimeout(() => {
133
+ if (!resolved) {
134
+ debug('Command timeout after 15s');
135
+ socket.destroy();
136
+ reject(new Error('Command timeout'));
137
+ }
138
+ }, 15000);
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Send a command, ensuring daemon is running first
144
+ */
145
+ export async function send(command: Record<string, unknown>): Promise<Response> {
146
+ const startTime = Date.now();
147
+ await ensureDaemon();
148
+ debug('ensureDaemon took', Date.now() - startTime, 'ms');
149
+ return sendCommand(command);
150
+ }
package/src/daemon.ts ADDED
@@ -0,0 +1,187 @@
1
+ import * as net from 'net';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { BrowserManager } from './browser.js';
6
+ import { parseCommand, serializeResponse, errorResponse } from './protocol.js';
7
+ import { executeCommand } from './actions.js';
8
+
9
+ // Session support - each session gets its own socket/pid
10
+ let currentSession = process.env.AGENT_BROWSER_SESSION || 'default';
11
+
12
+ /**
13
+ * Set the current session
14
+ */
15
+ export function setSession(session: string): void {
16
+ currentSession = session;
17
+ }
18
+
19
+ /**
20
+ * Get the current session
21
+ */
22
+ export function getSession(): string {
23
+ return currentSession;
24
+ }
25
+
26
+ /**
27
+ * Get the socket path for the current session
28
+ */
29
+ export function getSocketPath(session?: string): string {
30
+ const sess = session ?? currentSession;
31
+ return path.join(os.tmpdir(), `agent-browser-${sess}.sock`);
32
+ }
33
+
34
+ /**
35
+ * Get the PID file path for the current session
36
+ */
37
+ export function getPidFile(session?: string): string {
38
+ const sess = session ?? currentSession;
39
+ return path.join(os.tmpdir(), `agent-browser-${sess}.pid`);
40
+ }
41
+
42
+ /**
43
+ * Check if daemon is running for the current session
44
+ */
45
+ export function isDaemonRunning(session?: string): boolean {
46
+ const pidFile = getPidFile(session);
47
+ if (!fs.existsSync(pidFile)) return false;
48
+
49
+ try {
50
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
51
+ // Check if process exists
52
+ process.kill(pid, 0);
53
+ return true;
54
+ } catch {
55
+ // Process doesn't exist, clean up stale files
56
+ cleanupSocket(session);
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Clean up socket and PID file for the current session
63
+ */
64
+ export function cleanupSocket(session?: string): void {
65
+ const socketPath = getSocketPath(session);
66
+ const pidFile = getPidFile(session);
67
+ try {
68
+ if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath);
69
+ if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
70
+ } catch {
71
+ // Ignore cleanup errors
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Start the daemon server
77
+ */
78
+ export async function startDaemon(): Promise<void> {
79
+ // Clean up any stale socket
80
+ cleanupSocket();
81
+
82
+ const browser = new BrowserManager();
83
+ let shuttingDown = false;
84
+
85
+ const server = net.createServer((socket) => {
86
+ let buffer = '';
87
+
88
+ socket.on('data', async (data) => {
89
+ buffer += data.toString();
90
+
91
+ // Process complete lines
92
+ while (buffer.includes('\n')) {
93
+ const newlineIdx = buffer.indexOf('\n');
94
+ const line = buffer.substring(0, newlineIdx);
95
+ buffer = buffer.substring(newlineIdx + 1);
96
+
97
+ if (!line.trim()) continue;
98
+
99
+ try {
100
+ const parseResult = parseCommand(line);
101
+
102
+ if (!parseResult.success) {
103
+ const resp = errorResponse(parseResult.id ?? 'unknown', parseResult.error);
104
+ socket.write(serializeResponse(resp) + '\n');
105
+ continue;
106
+ }
107
+
108
+ // Auto-launch browser if not already launched and this isn't a launch command
109
+ if (
110
+ !browser.isLaunched() &&
111
+ parseResult.command.action !== 'launch' &&
112
+ parseResult.command.action !== 'close'
113
+ ) {
114
+ await browser.launch({ id: 'auto', action: 'launch', headless: true });
115
+ }
116
+
117
+ // Handle close command specially
118
+ if (parseResult.command.action === 'close') {
119
+ const response = await executeCommand(parseResult.command, browser);
120
+ socket.write(serializeResponse(response) + '\n');
121
+
122
+ if (!shuttingDown) {
123
+ shuttingDown = true;
124
+ setTimeout(() => {
125
+ server.close();
126
+ cleanupSocket();
127
+ process.exit(0);
128
+ }, 100);
129
+ }
130
+ return;
131
+ }
132
+
133
+ const response = await executeCommand(parseResult.command, browser);
134
+ socket.write(serializeResponse(response) + '\n');
135
+ } catch (err) {
136
+ const message = err instanceof Error ? err.message : String(err);
137
+ socket.write(serializeResponse(errorResponse('error', message)) + '\n');
138
+ }
139
+ }
140
+ });
141
+
142
+ socket.on('error', () => {
143
+ // Client disconnected, ignore
144
+ });
145
+ });
146
+
147
+ const socketPath = getSocketPath();
148
+ const pidFile = getPidFile();
149
+
150
+ // Write PID file before listening
151
+ fs.writeFileSync(pidFile, process.pid.toString());
152
+
153
+ server.listen(socketPath, () => {
154
+ // Daemon is ready
155
+ });
156
+
157
+ server.on('error', (err) => {
158
+ console.error('Server error:', err);
159
+ cleanupSocket();
160
+ process.exit(1);
161
+ });
162
+
163
+ // Handle shutdown signals
164
+ const shutdown = async () => {
165
+ if (shuttingDown) return;
166
+ shuttingDown = true;
167
+ await browser.close();
168
+ server.close();
169
+ cleanupSocket();
170
+ process.exit(0);
171
+ };
172
+
173
+ process.on('SIGINT', shutdown);
174
+ process.on('SIGTERM', shutdown);
175
+
176
+ // Keep process alive
177
+ process.stdin.resume();
178
+ }
179
+
180
+ // Run daemon if this is the entry point
181
+ if (process.argv[1]?.endsWith('daemon.js') || process.env.AGENT_BROWSER_DAEMON === '1') {
182
+ startDaemon().catch((err) => {
183
+ console.error('Daemon error:', err);
184
+ cleanupSocket();
185
+ process.exit(1);
186
+ });
187
+ }