@dyyz1993/agent-browser 0.13.2 → 0.24.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.
Files changed (164) hide show
  1. package/README.md +108 -0
  2. package/bin/agent-browser-darwin-arm64 +0 -0
  3. package/bin/agent-browser-darwin-x64 +0 -0
  4. package/bin/agent-browser-linux-arm64 +0 -0
  5. package/bin/agent-browser-linux-x64 +0 -0
  6. package/bin/agent-browser-win32-x64.exe +0 -0
  7. package/dist/__tests__/e2e/utils/test-helpers.d.ts +1 -0
  8. package/dist/__tests__/e2e/utils/test-helpers.d.ts.map +1 -1
  9. package/dist/__tests__/e2e/utils/test-helpers.js +14 -1
  10. package/dist/__tests__/e2e/utils/test-helpers.js.map +1 -1
  11. package/dist/__tests__/utils/free-port.d.ts +2 -0
  12. package/dist/__tests__/utils/free-port.d.ts.map +1 -0
  13. package/dist/__tests__/utils/free-port.js +18 -0
  14. package/dist/__tests__/utils/free-port.js.map +1 -0
  15. package/dist/__tests__/utils/parseCli.d.ts.map +1 -1
  16. package/dist/__tests__/utils/parseCli.js +83 -9
  17. package/dist/__tests__/utils/parseCli.js.map +1 -1
  18. package/dist/actions.d.ts.map +1 -1
  19. package/dist/actions.js +298 -9
  20. package/dist/actions.js.map +1 -1
  21. package/dist/browser.d.ts +11 -1
  22. package/dist/browser.d.ts.map +1 -1
  23. package/dist/browser.js +75 -19
  24. package/dist/browser.js.map +1 -1
  25. package/dist/cli/commands.d.ts.map +1 -1
  26. package/dist/cli/commands.js +172 -15
  27. package/dist/cli/commands.js.map +1 -1
  28. package/dist/cli/connection.d.ts +13 -0
  29. package/dist/cli/connection.d.ts.map +1 -1
  30. package/dist/cli/connection.js +137 -48
  31. package/dist/cli/connection.js.map +1 -1
  32. package/dist/cli/flags.d.ts.map +1 -1
  33. package/dist/cli/flags.js +0 -1
  34. package/dist/cli/flags.js.map +1 -1
  35. package/dist/cli/help.d.ts.map +1 -1
  36. package/dist/cli/help.js +63 -22
  37. package/dist/cli/help.js.map +1 -1
  38. package/dist/cli/output.d.ts.map +1 -1
  39. package/dist/cli/output.js +0 -32
  40. package/dist/cli/output.js.map +1 -1
  41. package/dist/cli.js +20 -2
  42. package/dist/cli.js.map +1 -1
  43. package/dist/daemon.d.ts +1 -0
  44. package/dist/daemon.d.ts.map +1 -1
  45. package/dist/daemon.js +291 -264
  46. package/dist/daemon.js.map +1 -1
  47. package/dist/diff.d.ts.map +1 -1
  48. package/dist/diff.js +1 -1
  49. package/dist/diff.js.map +1 -1
  50. package/dist/flow/exporters/cypress.d.ts +9 -0
  51. package/dist/flow/exporters/cypress.d.ts.map +1 -0
  52. package/dist/flow/exporters/cypress.js +256 -0
  53. package/dist/flow/exporters/cypress.js.map +1 -0
  54. package/dist/flow/exporters/index.d.ts +6 -0
  55. package/dist/flow/exporters/index.d.ts.map +1 -0
  56. package/dist/flow/exporters/index.js +5 -0
  57. package/dist/flow/exporters/index.js.map +1 -0
  58. package/dist/flow/exporters/playwright.d.ts +20 -0
  59. package/dist/flow/exporters/playwright.d.ts.map +1 -0
  60. package/dist/flow/exporters/playwright.js +175 -0
  61. package/dist/flow/exporters/playwright.js.map +1 -0
  62. package/dist/flow/exporters/python.d.ts +20 -0
  63. package/dist/flow/exporters/python.d.ts.map +1 -0
  64. package/dist/flow/exporters/python.js +163 -0
  65. package/dist/flow/exporters/python.js.map +1 -0
  66. package/dist/flow/exporters/selenium.d.ts +9 -0
  67. package/dist/flow/exporters/selenium.d.ts.map +1 -0
  68. package/dist/flow/exporters/selenium.js +298 -0
  69. package/dist/flow/exporters/selenium.js.map +1 -0
  70. package/dist/flow/exporters/types.d.ts +13 -0
  71. package/dist/flow/exporters/types.d.ts.map +1 -0
  72. package/dist/flow/exporters/types.js +2 -0
  73. package/dist/flow/exporters/types.js.map +1 -0
  74. package/dist/flow/flow-executor.d.ts +57 -0
  75. package/dist/flow/flow-executor.d.ts.map +1 -0
  76. package/dist/flow/flow-executor.js +1263 -0
  77. package/dist/flow/flow-executor.js.map +1 -0
  78. package/dist/flow/index.d.ts +15 -0
  79. package/dist/flow/index.d.ts.map +1 -0
  80. package/dist/flow/index.js +10 -0
  81. package/dist/flow/index.js.map +1 -0
  82. package/dist/flow/output.d.ts +11 -0
  83. package/dist/flow/output.d.ts.map +1 -0
  84. package/dist/flow/output.js +84 -0
  85. package/dist/flow/output.js.map +1 -0
  86. package/dist/flow/plugin-system.d.ts +48 -0
  87. package/dist/flow/plugin-system.d.ts.map +1 -0
  88. package/dist/flow/plugin-system.js +132 -0
  89. package/dist/flow/plugin-system.js.map +1 -0
  90. package/dist/flow/plugins/file-output-plugin.d.ts +8 -0
  91. package/dist/flow/plugins/file-output-plugin.d.ts.map +1 -0
  92. package/dist/flow/plugins/file-output-plugin.js +31 -0
  93. package/dist/flow/plugins/file-output-plugin.js.map +1 -0
  94. package/dist/flow/plugins/index.d.ts +4 -0
  95. package/dist/flow/plugins/index.d.ts.map +1 -0
  96. package/dist/flow/plugins/index.js +4 -0
  97. package/dist/flow/plugins/index.js.map +1 -0
  98. package/dist/flow/plugins/logging-plugin.d.ts +7 -0
  99. package/dist/flow/plugins/logging-plugin.d.ts.map +1 -0
  100. package/dist/flow/plugins/logging-plugin.js +40 -0
  101. package/dist/flow/plugins/logging-plugin.js.map +1 -0
  102. package/dist/flow/plugins/webhook-plugin.d.ts +7 -0
  103. package/dist/flow/plugins/webhook-plugin.d.ts.map +1 -0
  104. package/dist/flow/plugins/webhook-plugin.js +24 -0
  105. package/dist/flow/plugins/webhook-plugin.js.map +1 -0
  106. package/dist/flow/presets/index.d.ts +10 -0
  107. package/dist/flow/presets/index.d.ts.map +1 -0
  108. package/dist/flow/presets/index.js +29 -0
  109. package/dist/flow/presets/index.js.map +1 -0
  110. package/dist/flow/recorder-to-flow.d.ts +70 -0
  111. package/dist/flow/recorder-to-flow.d.ts.map +1 -0
  112. package/dist/flow/recorder-to-flow.js +392 -0
  113. package/dist/flow/recorder-to-flow.js.map +1 -0
  114. package/dist/flow/site-manager.d.ts +24 -0
  115. package/dist/flow/site-manager.d.ts.map +1 -0
  116. package/dist/flow/site-manager.js +125 -0
  117. package/dist/flow/site-manager.js.map +1 -0
  118. package/dist/flow/types.d.ts +196 -0
  119. package/dist/flow/types.d.ts.map +1 -0
  120. package/dist/flow/types.js +2 -0
  121. package/dist/flow/types.js.map +1 -0
  122. package/dist/flow/yaml-parser.d.ts +15 -0
  123. package/dist/flow/yaml-parser.d.ts.map +1 -0
  124. package/dist/flow/yaml-parser.js +216 -0
  125. package/dist/flow/yaml-parser.js.map +1 -0
  126. package/dist/human-mouse.d.ts.map +1 -1
  127. package/dist/protocol.d.ts.map +1 -1
  128. package/dist/protocol.js +15 -11
  129. package/dist/protocol.js.map +1 -1
  130. package/dist/rc-config.d.ts.map +1 -1
  131. package/dist/rc-config.js +1 -2
  132. package/dist/rc-config.js.map +1 -1
  133. package/dist/recorder/inject.js +730 -332
  134. package/dist/snapshot-store.d.ts +83 -0
  135. package/dist/snapshot-store.d.ts.map +1 -0
  136. package/dist/snapshot-store.js +112 -0
  137. package/dist/snapshot-store.js.map +1 -0
  138. package/dist/snapshot.d.ts +6 -7
  139. package/dist/snapshot.d.ts.map +1 -1
  140. package/dist/snapshot.js +471 -17
  141. package/dist/snapshot.js.map +1 -1
  142. package/dist/stream-server-standalone.d.ts.map +1 -1
  143. package/dist/stream-server-standalone.js.map +1 -1
  144. package/dist/stream-server.d.ts.map +1 -1
  145. package/dist/stream-server.js +38 -13
  146. package/dist/stream-server.js.map +1 -1
  147. package/dist/test-live.js +5 -5
  148. package/dist/test-live.js.map +1 -1
  149. package/dist/types.d.ts +13 -9
  150. package/dist/types.d.ts.map +1 -1
  151. package/dist/types.js.map +1 -1
  152. package/dist/viewer-script.d.ts.map +1 -1
  153. package/dist/viewer-script.js +12 -6
  154. package/dist/viewer-script.js.map +1 -1
  155. package/package.json +18 -5
  156. package/skills/agent-browser/SKILL.md +151 -3
  157. package/dist/ios-actions.d.ts +0 -11
  158. package/dist/ios-actions.d.ts.map +0 -1
  159. package/dist/ios-actions.js +0 -228
  160. package/dist/ios-actions.js.map +0 -1
  161. package/dist/ios-manager.d.ts +0 -266
  162. package/dist/ios-manager.d.ts.map +0 -1
  163. package/dist/ios-manager.js +0 -1076
  164. package/dist/ios-manager.js.map +0 -1
package/dist/daemon.js CHANGED
@@ -4,10 +4,8 @@ import * as path from 'path';
4
4
  import * as os from 'os';
5
5
  import { randomUUID } from 'crypto';
6
6
  import { BrowserManager } from './browser.js';
7
- import { IOSManager } from './ios-manager.js';
8
7
  import { parseCommand, serializeResponse, errorResponse, successResponse } from './protocol.js';
9
8
  import { executeCommand } from './actions.js';
10
- import { executeIOSCommand } from './ios-actions.js';
11
9
  import { getExecutablePath } from './rc-config.js';
12
10
  import { StreamServerProxy, getStreamServerIpcPath } from './stream-server.js';
13
11
  const isWindows = process.platform === 'win32';
@@ -15,6 +13,7 @@ let currentSession = process.env.AGENT_BROWSER_SESSION || 'default';
15
13
  let currentInstanceId = randomUUID().substring(0, 8);
16
14
  let streamServerProxy = null;
17
15
  let lastUrl = null;
16
+ let lastActivityAt = Date.now();
18
17
  const STREAM_SERVER_PID_FILE = 'stream-server.pid';
19
18
  const INPUT_FILL_DEBOUNCE_MS = 60;
20
19
  const inputFillDebounceMap = new Map();
@@ -59,17 +58,19 @@ export function getSession() {
59
58
  export function getInstanceId() {
60
59
  return currentInstanceId;
61
60
  }
61
+ export function getLastActivityAt() {
62
+ return lastActivityAt;
63
+ }
62
64
  /**
63
65
  * Get port number for TCP mode (Windows)
64
66
  * Uses a hash of the session name to get a consistent port
65
67
  */
66
68
  function getPortForSession(session) {
67
- let hash = 0;
69
+ let hash = 2166136261;
68
70
  for (let i = 0; i < session.length; i++) {
69
- hash = (hash << 5) - hash + session.charCodeAt(i);
70
- hash |= 0;
71
+ hash ^= session.charCodeAt(i);
72
+ hash = Math.imul(hash, 16777619);
71
73
  }
72
- // Port range 49152-65535 (dynamic/private ports)
73
74
  return 49152 + (Math.abs(hash) % 16383);
74
75
  }
75
76
  /**
@@ -78,8 +79,9 @@ function getPortForSession(session) {
78
79
  */
79
80
  export function getAppDir() {
80
81
  // 1. XDG_RUNTIME_DIR (Linux standard)
81
- if (process.env.XDG_RUNTIME_DIR) {
82
- return path.join(process.env.XDG_RUNTIME_DIR, 'agent-browser');
82
+ const runtimeDir = process.env.XDG_RUNTIME_DIR;
83
+ if (runtimeDir) {
84
+ return path.join(runtimeDir, 'agent-browser');
83
85
  }
84
86
  // 2. Home directory fallback (like Docker Desktop's ~/.docker/run/)
85
87
  const homeDir = os.homedir();
@@ -203,19 +205,36 @@ export function cleanupSocket(session) {
203
205
  const pidFile = getPidFile(session);
204
206
  const streamPortFile = getStreamPortFile(session);
205
207
  try {
206
- if (fs.existsSync(pidFile))
208
+ // Remove stale files, ignoring ENOENT (avoid TOCTOU race)
209
+ try {
207
210
  fs.unlinkSync(pidFile);
208
- if (fs.existsSync(streamPortFile))
211
+ }
212
+ catch (_e) {
213
+ /* not found */
214
+ }
215
+ try {
209
216
  fs.unlinkSync(streamPortFile);
217
+ }
218
+ catch (_e) {
219
+ /* not found */
220
+ }
210
221
  if (isWindows) {
211
222
  const portFile = getPortFile(session);
212
- if (fs.existsSync(portFile))
223
+ try {
213
224
  fs.unlinkSync(portFile);
225
+ }
226
+ catch (_e) {
227
+ /* not found */
228
+ }
214
229
  }
215
230
  else {
216
231
  const socketPath = getSocketPath(session);
217
- if (fs.existsSync(socketPath))
232
+ try {
218
233
  fs.unlinkSync(socketPath);
234
+ }
235
+ catch (_e) {
236
+ /* not found */
237
+ }
219
238
  }
220
239
  }
221
240
  catch {
@@ -236,10 +255,9 @@ export async function startDaemon(options) {
236
255
  }
237
256
  cleanupSocket();
238
257
  const provider = options?.provider ?? process.env.AGENT_BROWSER_PROVIDER;
239
- const isIOS = provider === 'ios';
240
- const manager = isIOS ? new IOSManager() : new BrowserManager();
258
+ const manager = new BrowserManager();
241
259
  let shuttingDown = false;
242
- if (!isIOS && manager instanceof BrowserManager) {
260
+ {
243
261
  const ipcPath = getStreamServerIpcPath();
244
262
  if (fs.existsSync(ipcPath)) {
245
263
  streamServerProxy = new StreamServerProxy(manager);
@@ -259,287 +277,296 @@ export async function startDaemon(options) {
259
277
  const server = net.createServer((socket) => {
260
278
  let buffer = '';
261
279
  let httpChecked = false;
262
- socket.on('data', async (data) => {
263
- buffer += data.toString();
264
- // Security: Detect and reject HTTP requests to prevent cross-origin attacks.
265
- // Browsers using fetch() must send HTTP headers (e.g., "POST / HTTP/1.1"),
266
- // while legitimate clients send raw JSON starting with "{".
267
- if (!httpChecked) {
268
- httpChecked = true;
269
- const trimmed = buffer.trimStart();
270
- if (/^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|CONNECT|TRACE)\s/i.test(trimmed)) {
271
- socket.destroy();
272
- return;
273
- }
274
- }
275
- // Process complete lines
276
- while (buffer.includes('\n')) {
277
- const newlineIdx = buffer.indexOf('\n');
278
- const line = buffer.substring(0, newlineIdx);
279
- buffer = buffer.substring(newlineIdx + 1);
280
- if (!line.trim())
281
- continue;
282
- // Handle custom actions before schema validation (not in standard Zod union)
283
- // Viewer sends messages with 'type' field, standalone commands use 'action' field.
284
- // Normalize to support both.
285
- if (line.trim()) {
286
- try {
287
- const quickParse = JSON.parse(line);
288
- const action = quickParse.action || quickParse.type;
289
- if (quickParse &&
290
- action === 'inject_focus_listener' &&
291
- manager instanceof BrowserManager) {
292
- try {
293
- await manager.injectFocusListener((data) => {
294
- try {
295
- socket.write(JSON.stringify(data) + '\n');
296
- }
297
- catch (_) { }
298
- });
299
- socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
300
- }
301
- catch (err) {
302
- const message = err instanceof Error ? err.message : String(err);
303
- socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
280
+ let processing = false;
281
+ async function processBuffer() {
282
+ if (processing)
283
+ return;
284
+ processing = true;
285
+ try {
286
+ while (buffer.includes('\n')) {
287
+ const newlineIdx = buffer.indexOf('\n');
288
+ const line = buffer.substring(0, newlineIdx);
289
+ buffer = buffer.substring(newlineIdx + 1);
290
+ if (!line.trim())
291
+ continue;
292
+ // Handle custom actions before schema validation (not in standard Zod union)
293
+ // Viewer sends messages with 'type' field, standalone commands use 'action' field.
294
+ // Normalize to support both.
295
+ if (line.trim()) {
296
+ try {
297
+ const quickParse = JSON.parse(line);
298
+ const action = quickParse.action || quickParse.type;
299
+ if (quickParse &&
300
+ action === 'inject_focus_listener' &&
301
+ manager instanceof BrowserManager) {
302
+ try {
303
+ await manager.injectFocusListener((data) => {
304
+ try {
305
+ socket.write(JSON.stringify(data) + '\n');
306
+ }
307
+ catch (_e) {
308
+ // Socket write to disconnected client, non-fatal
309
+ }
310
+ });
311
+ socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
312
+ }
313
+ catch (err) {
314
+ const message = err instanceof Error ? err.message : String(err);
315
+ socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
316
+ }
317
+ continue;
304
318
  }
305
- continue;
306
- }
307
- if (quickParse && action === 'input_fill' && manager instanceof BrowserManager) {
308
- const selector = quickParse.selector || '';
309
- const text = quickParse.text || '';
310
- debounceInputFill(socket, manager, String(quickParse.id), selector, text);
311
- continue;
312
- }
313
- if (quickParse &&
314
- (action === 'blur_element' || action === 'input_blur_element') &&
315
- manager instanceof BrowserManager) {
316
- try {
319
+ if (quickParse && action === 'input_fill' && manager instanceof BrowserManager) {
317
320
  const selector = quickParse.selector || '';
318
- await manager.blurElement(selector);
319
- socket.write(serializeResponse(successResponse(quickParse.id, { blurred: true, selector })) +
320
- '\n');
321
+ const text = quickParse.text || '';
322
+ debounceInputFill(socket, manager, String(quickParse.id), selector, text);
323
+ continue;
321
324
  }
322
- catch (err) {
323
- const message = err instanceof Error ? err.message : String(err);
324
- socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
325
+ if (quickParse &&
326
+ (action === 'blur_element' || action === 'input_blur_element') &&
327
+ manager instanceof BrowserManager) {
328
+ try {
329
+ const selector = quickParse.selector || '';
330
+ await manager.blurElement(selector);
331
+ socket.write(serializeResponse(successResponse(quickParse.id, { blurred: true, selector })) +
332
+ '\n');
333
+ }
334
+ catch (err) {
335
+ const message = err instanceof Error ? err.message : String(err);
336
+ socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
337
+ }
338
+ continue;
325
339
  }
326
- continue;
327
- }
328
- if (quickParse && action === 'input_mouse' && manager instanceof BrowserManager) {
329
- try {
330
- await manager.injectMouseEvent({
331
- type: quickParse.eventType,
332
- x: quickParse.x ?? 0,
333
- y: quickParse.y ?? 0,
334
- button: quickParse.button,
335
- clickCount: quickParse.clickCount,
336
- deltaX: quickParse.deltaX,
337
- deltaY: quickParse.deltaY,
338
- modifiers: quickParse.modifiers,
339
- });
340
- socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
340
+ if (quickParse && action === 'input_mouse' && manager instanceof BrowserManager) {
341
+ try {
342
+ await manager.injectMouseEvent({
343
+ type: quickParse.eventType,
344
+ x: quickParse.x ?? 0,
345
+ y: quickParse.y ?? 0,
346
+ button: quickParse.button,
347
+ clickCount: quickParse.clickCount,
348
+ deltaX: quickParse.deltaX,
349
+ deltaY: quickParse.deltaY,
350
+ modifiers: quickParse.modifiers,
351
+ });
352
+ socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
353
+ }
354
+ catch (err) {
355
+ const message = err instanceof Error ? err.message : String(err);
356
+ socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
357
+ }
358
+ continue;
341
359
  }
342
- catch (err) {
343
- const message = err instanceof Error ? err.message : String(err);
344
- socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
360
+ if (quickParse && action === 'input_keyboard' && manager instanceof BrowserManager) {
361
+ try {
362
+ await manager.injectKeyboardEvent({
363
+ type: quickParse.eventType,
364
+ key: quickParse.key,
365
+ code: quickParse.code,
366
+ text: quickParse.text,
367
+ modifiers: quickParse.modifiers,
368
+ });
369
+ socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
370
+ }
371
+ catch (err) {
372
+ const message = err instanceof Error ? err.message : String(err);
373
+ socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
374
+ }
375
+ continue;
345
376
  }
377
+ if (quickParse &&
378
+ action === 'keyboard_insert_text' &&
379
+ manager instanceof BrowserManager) {
380
+ try {
381
+ const text = quickParse.text || '';
382
+ await manager.insertText(text);
383
+ socket.write(serializeResponse(successResponse(quickParse.id, { inserted: true })) + '\n');
384
+ }
385
+ catch (err) {
386
+ const message = err instanceof Error ? err.message : String(err);
387
+ socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
388
+ }
389
+ continue;
390
+ }
391
+ if (quickParse && action === '_ping') {
392
+ try {
393
+ const tabList = manager instanceof BrowserManager && manager.isLaunched()
394
+ ? await manager.listTabs()
395
+ : [];
396
+ socket.write(serializeResponse(successResponse(quickParse.id, {
397
+ session: currentSession,
398
+ lastActivityAt,
399
+ tabs: tabList,
400
+ })) + '\n');
401
+ }
402
+ catch (err) {
403
+ const message = err instanceof Error ? err.message : String(err);
404
+ socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
405
+ }
406
+ continue;
407
+ }
408
+ }
409
+ catch (_) {
410
+ /* not JSON, fall through to normal parsing */
411
+ }
412
+ }
413
+ try {
414
+ const parseResult = parseCommand(line);
415
+ if (!parseResult.success) {
416
+ const resp = errorResponse(parseResult.id ?? 'unknown', parseResult.error);
417
+ socket.write(serializeResponse(resp) + '\n');
346
418
  continue;
347
419
  }
348
- if (quickParse && action === 'input_keyboard' && manager instanceof BrowserManager) {
349
- try {
350
- await manager.injectKeyboardEvent({
351
- type: quickParse.eventType,
352
- key: quickParse.key,
353
- code: quickParse.code,
354
- text: quickParse.text,
355
- modifiers: quickParse.modifiers,
420
+ // Auto-launch if not already launched and this isn't a launch/close command
421
+ if (!manager.isLaunched() &&
422
+ parseResult.command.action !== 'launch' &&
423
+ parseResult.command.action !== 'close') {
424
+ if (manager instanceof BrowserManager) {
425
+ // Auto-launch desktop browser
426
+ const extensions = process.env.AGENT_BROWSER_EXTENSIONS
427
+ ? (process.env.AGENT_BROWSER_EXTENSIONS || '')
428
+ .split(',')
429
+ .map((p) => p.trim())
430
+ .filter(Boolean)
431
+ : undefined;
432
+ // Parse args from env (comma or newline separated)
433
+ const argsEnv = process.env.AGENT_BROWSER_ARGS;
434
+ const args = argsEnv
435
+ ? argsEnv
436
+ .split(/[,\n]/)
437
+ .map((a) => a.trim())
438
+ .filter((a) => a.length > 0)
439
+ : undefined;
440
+ // Parse proxy from env
441
+ const proxyServer = process.env.AGENT_BROWSER_PROXY;
442
+ const proxyBypass = process.env.AGENT_BROWSER_PROXY_BYPASS;
443
+ const proxy = proxyServer
444
+ ? {
445
+ server: proxyServer,
446
+ ...(proxyBypass && { bypass: proxyBypass }),
447
+ }
448
+ : undefined;
449
+ const ignoreHTTPSErrors = process.env.AGENT_BROWSER_IGNORE_HTTPS_ERRORS === '1';
450
+ const allowFileAccess = process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1';
451
+ await manager.launch({
452
+ id: 'auto',
453
+ action: 'launch',
454
+ headless: process.env.AGENT_BROWSER_HEADED !== '1',
455
+ executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH || getExecutablePath(),
456
+ extensions: extensions,
457
+ profile: process.env.AGENT_BROWSER_PROFILE,
458
+ storageState: process.env.AGENT_BROWSER_STATE,
459
+ args,
460
+ userAgent: process.env.AGENT_BROWSER_USER_AGENT,
461
+ proxy,
462
+ ignoreHTTPSErrors: ignoreHTTPSErrors,
463
+ allowFileAccess: allowFileAccess,
356
464
  });
357
- socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
358
465
  }
359
- catch (err) {
360
- const message = err instanceof Error ? err.message : String(err);
361
- socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
466
+ }
467
+ // Handle close command specially - shuts down daemon
468
+ if (parseResult.command.action === 'close') {
469
+ const response = await executeCommand(parseResult.command, manager);
470
+ socket.write(serializeResponse(response) + '\n');
471
+ if (!shuttingDown) {
472
+ shuttingDown = true;
473
+ // 先断开 StreamServer 连接,发送 unregister 消息
474
+ if (streamServerProxy) {
475
+ await streamServerProxy.disconnect();
476
+ streamServerProxy = null;
477
+ }
478
+ setTimeout(() => {
479
+ server.close();
480
+ cleanupSocket();
481
+ process.exit(0);
482
+ }, 100);
362
483
  }
363
- continue;
484
+ return;
364
485
  }
365
- if (quickParse &&
366
- action === 'keyboard_insert_text' &&
486
+ // Handle inject_focus_listener: set up focus event bridge to stream-server
487
+ if (parseResult.command.action === 'inject_focus_listener' &&
367
488
  manager instanceof BrowserManager) {
368
489
  try {
369
- const text = quickParse.text || '';
370
- await manager.insertText(text);
371
- socket.write(serializeResponse(successResponse(quickParse.id, { inserted: true })) + '\n');
490
+ await manager.injectFocusListener((data) => {
491
+ try {
492
+ socket.write(JSON.stringify(data) + '\n');
493
+ }
494
+ catch (_e) {
495
+ // Socket write to disconnected client, non-fatal
496
+ }
497
+ });
498
+ socket.write(serializeResponse(successResponse(parseResult.command.id, { injected: true })) +
499
+ '\n');
372
500
  }
373
501
  catch (err) {
374
502
  const message = err instanceof Error ? err.message : String(err);
375
- socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
503
+ socket.write(serializeResponse(errorResponse(parseResult.command.id, message)) + '\n');
376
504
  }
377
505
  continue;
378
506
  }
379
- }
380
- catch (_) {
381
- /* not JSON, fall through to normal parsing */
382
- }
383
- }
384
- try {
385
- const parseResult = parseCommand(line);
386
- if (!parseResult.success) {
387
- const resp = errorResponse(parseResult.id ?? 'unknown', parseResult.error);
388
- socket.write(serializeResponse(resp) + '\n');
389
- continue;
390
- }
391
- // Handle device_list specially - it works without a session and always uses IOSManager
392
- if (parseResult.command.action === 'device_list') {
393
- const iosManager = new IOSManager();
394
- try {
395
- const devices = await iosManager.listAllDevices();
396
- const response = {
397
- id: parseResult.command.id,
398
- success: true,
399
- data: { devices },
400
- };
401
- socket.write(serializeResponse(response) + '\n');
402
- }
403
- catch (err) {
404
- const message = err instanceof Error ? err.message : String(err);
405
- socket.write(serializeResponse(errorResponse(parseResult.command.id, message)) + '\n');
406
- }
407
- continue;
408
- }
409
- // Auto-launch if not already launched and this isn't a launch/close command
410
- if (!manager.isLaunched() &&
411
- parseResult.command.action !== 'launch' &&
412
- parseResult.command.action !== 'close') {
413
- if (isIOS && manager instanceof IOSManager) {
414
- // Auto-launch iOS Safari
415
- // Check for device in command first (for reused daemons), then fall back to env vars
416
- const cmd = parseResult.command;
417
- const iosDevice = cmd.iosDevice || process.env.AGENT_BROWSER_IOS_DEVICE;
418
- await manager.launch({
419
- device: iosDevice,
420
- udid: process.env.AGENT_BROWSER_IOS_UDID,
421
- });
422
- }
423
- else if (manager instanceof BrowserManager) {
424
- // Auto-launch desktop browser
425
- const extensions = process.env.AGENT_BROWSER_EXTENSIONS
426
- ? process.env.AGENT_BROWSER_EXTENSIONS.split(',')
427
- .map((p) => p.trim())
428
- .filter(Boolean)
429
- : undefined;
430
- // Parse args from env (comma or newline separated)
431
- const argsEnv = process.env.AGENT_BROWSER_ARGS;
432
- const args = argsEnv
433
- ? argsEnv
434
- .split(/[,\n]/)
435
- .map((a) => a.trim())
436
- .filter((a) => a.length > 0)
437
- : undefined;
438
- // Parse proxy from env
439
- const proxyServer = process.env.AGENT_BROWSER_PROXY;
440
- const proxyBypass = process.env.AGENT_BROWSER_PROXY_BYPASS;
441
- const proxy = proxyServer
442
- ? {
443
- server: proxyServer,
444
- ...(proxyBypass && { bypass: proxyBypass }),
507
+ lastActivityAt = Date.now();
508
+ // Execute command with appropriate handler
509
+ let response = await executeCommand(parseResult.command, manager);
510
+ if (response.success && manager instanceof BrowserManager && manager.isLaunched()) {
511
+ try {
512
+ const currentUrl = manager.getPage().url();
513
+ if (lastUrl !== null && currentUrl !== lastUrl) {
514
+ const urlTip = `URL changed: ${lastUrl} -> ${currentUrl}`;
515
+ const existingTips = response.tips;
516
+ if (existingTips) {
517
+ const tipsArray = Array.isArray(existingTips) ? existingTips : [existingTips];
518
+ response.tips = [urlTip, ...tipsArray];
519
+ }
520
+ else {
521
+ response.tips = [urlTip];
522
+ }
445
523
  }
446
- : undefined;
447
- const ignoreHTTPSErrors = process.env.AGENT_BROWSER_IGNORE_HTTPS_ERRORS === '1';
448
- const allowFileAccess = process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1';
449
- await manager.launch({
450
- id: 'auto',
451
- action: 'launch',
452
- headless: process.env.AGENT_BROWSER_HEADED !== '1',
453
- executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH || getExecutablePath(),
454
- extensions: extensions,
455
- profile: process.env.AGENT_BROWSER_PROFILE,
456
- storageState: process.env.AGENT_BROWSER_STATE,
457
- args,
458
- userAgent: process.env.AGENT_BROWSER_USER_AGENT,
459
- proxy,
460
- ignoreHTTPSErrors: ignoreHTTPSErrors,
461
- allowFileAccess: allowFileAccess,
462
- });
463
- }
464
- }
465
- // Handle close command specially - shuts down daemon
466
- if (parseResult.command.action === 'close') {
467
- const response = isIOS && manager instanceof IOSManager
468
- ? await executeIOSCommand(parseResult.command, manager)
469
- : await executeCommand(parseResult.command, manager);
470
- socket.write(serializeResponse(response) + '\n');
471
- if (!shuttingDown) {
472
- shuttingDown = true;
473
- // 先断开 StreamServer 连接,发送 unregister 消息
474
- if (streamServerProxy) {
475
- await streamServerProxy.disconnect();
476
- streamServerProxy = null;
524
+ lastUrl = currentUrl;
477
525
  }
478
- setTimeout(() => {
479
- server.close();
480
- cleanupSocket();
481
- process.exit(0);
482
- }, 100);
483
- }
484
- return;
485
- }
486
- // Handle inject_focus_listener: set up focus event bridge to stream-server
487
- if (parseResult.command.action === 'inject_focus_listener' &&
488
- manager instanceof BrowserManager) {
489
- try {
490
- await manager.injectFocusListener((data) => {
491
- try {
492
- socket.write(JSON.stringify(data) + '\n');
493
- }
494
- catch (_) { }
495
- });
496
- socket.write(serializeResponse(successResponse(parseResult.command.id, { injected: true })) +
497
- '\n');
498
- }
499
- catch (err) {
500
- const message = err instanceof Error ? err.message : String(err);
501
- socket.write(serializeResponse(errorResponse(parseResult.command.id, message)) + '\n');
502
- }
503
- continue;
504
- }
505
- // Execute command with appropriate handler
506
- let response = isIOS && manager instanceof IOSManager
507
- ? await executeIOSCommand(parseResult.command, manager)
508
- : await executeCommand(parseResult.command, manager);
509
- if (response.success &&
510
- !isIOS &&
511
- manager instanceof BrowserManager &&
512
- manager.isLaunched()) {
513
- try {
514
- const currentUrl = manager.getPage().url();
515
- if (lastUrl !== null && currentUrl !== lastUrl) {
516
- const urlTip = `URL changed: ${lastUrl} -> ${currentUrl}`;
517
- const existingTips = response.tips;
518
- if (existingTips) {
519
- const tipsArray = Array.isArray(existingTips) ? existingTips : [existingTips];
520
- response.tips = [urlTip, ...tipsArray];
521
- }
522
- else {
523
- response.tips = [urlTip];
524
- }
526
+ catch {
527
+ // Page may not be available (e.g., after close)
525
528
  }
526
- lastUrl = currentUrl;
527
- }
528
- catch {
529
- // Page may not be available (e.g., after close)
530
529
  }
530
+ socket.write(serializeResponse(response) + '\n');
531
+ }
532
+ catch (err) {
533
+ const message = err instanceof Error ? err.message : String(err);
534
+ socket.write(serializeResponse(errorResponse('error', message)) + '\n');
531
535
  }
532
- socket.write(serializeResponse(response) + '\n');
533
536
  }
534
- catch (err) {
535
- const message = err instanceof Error ? err.message : String(err);
536
- socket.write(serializeResponse(errorResponse('error', message)) + '\n');
537
+ }
538
+ finally {
539
+ processing = false;
540
+ if (buffer.includes('\n')) {
541
+ processBuffer();
542
+ }
543
+ }
544
+ }
545
+ socket.on('data', (data) => {
546
+ buffer += data.toString();
547
+ // Security: Detect and reject HTTP requests to prevent cross-origin attacks.
548
+ // Browsers using fetch() must send HTTP headers (e.g., "POST / HTTP/1.1"),
549
+ // while legitimate clients send raw JSON starting with "{".
550
+ if (!httpChecked) {
551
+ httpChecked = true;
552
+ const trimmed = buffer.trimStart();
553
+ if (/^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|CONNECT|TRACE)\s/i.test(trimmed)) {
554
+ socket.destroy();
555
+ return;
537
556
  }
538
557
  }
558
+ processBuffer();
539
559
  });
540
560
  socket.on('error', () => {
541
561
  // Client disconnected, ignore
542
562
  });
563
+ socket.on('close', () => {
564
+ // Clear any pending debounce timers to prevent writes to closed socket
565
+ for (const [key, entry] of inputFillDebounceMap.entries()) {
566
+ clearTimeout(entry.timer);
567
+ inputFillDebounceMap.delete(key);
568
+ }
569
+ });
543
570
  });
544
571
  const pidFile = getPidFile();
545
572
  // Write PID file before listening