playwriter 0.0.63 → 0.0.89

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 (223) hide show
  1. package/dist/a11y-client.js +18 -8
  2. package/dist/aria-snapshot.d.ts +41 -3
  3. package/dist/aria-snapshot.d.ts.map +1 -1
  4. package/dist/aria-snapshot.js +134 -55
  5. package/dist/aria-snapshot.js.map +1 -1
  6. package/dist/aria-snapshot.test.js +5 -2
  7. package/dist/aria-snapshot.test.js.map +1 -1
  8. package/dist/aria-snapshot.unit.test.js +83 -41
  9. package/dist/aria-snapshot.unit.test.js.map +1 -1
  10. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
  11. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
  12. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
  13. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
  14. package/dist/bippy.js +1 -1
  15. package/dist/cdp-log.d.ts +1 -1
  16. package/dist/cdp-log.d.ts.map +1 -1
  17. package/dist/cdp-log.js +1 -1
  18. package/dist/cdp-log.js.map +1 -1
  19. package/dist/cdp-relay.d.ts.map +1 -1
  20. package/dist/cdp-relay.js +492 -298
  21. package/dist/cdp-relay.js.map +1 -1
  22. package/dist/cdp-session.d.ts.map +1 -1
  23. package/dist/cdp-session.js.map +1 -1
  24. package/dist/cdp-types.d.ts.map +1 -1
  25. package/dist/cdp-types.js +7 -7
  26. package/dist/cdp-types.js.map +1 -1
  27. package/dist/clean-html.d.ts.map +1 -1
  28. package/dist/clean-html.js +4 -5
  29. package/dist/clean-html.js.map +1 -1
  30. package/dist/cli.js +45 -27
  31. package/dist/cli.js.map +1 -1
  32. package/dist/create-logger.d.ts.map +1 -1
  33. package/dist/create-logger.js +3 -1
  34. package/dist/create-logger.js.map +1 -1
  35. package/dist/debugger-examples-types.d.ts.map +1 -1
  36. package/dist/debugger.d.ts.map +1 -1
  37. package/dist/debugger.js +1 -3
  38. package/dist/debugger.js.map +1 -1
  39. package/dist/diff-utils.d.ts.map +1 -1
  40. package/dist/diff-utils.js +1 -4
  41. package/dist/diff-utils.js.map +1 -1
  42. package/dist/editor-api.md +12 -2
  43. package/dist/editor-examples.d.ts +1 -1
  44. package/dist/editor-examples.d.ts.map +1 -1
  45. package/dist/editor-examples.js +1 -1
  46. package/dist/editor-examples.js.map +1 -1
  47. package/dist/editor.d.ts +1 -1
  48. package/dist/editor.d.ts.map +1 -1
  49. package/dist/editor.js +1 -1
  50. package/dist/editor.js.map +1 -1
  51. package/dist/executor.d.ts +26 -3
  52. package/dist/executor.d.ts.map +1 -1
  53. package/dist/executor.js +297 -64
  54. package/dist/executor.js.map +1 -1
  55. package/dist/executor.unit.test.js +38 -1
  56. package/dist/executor.unit.test.js.map +1 -1
  57. package/dist/extension-connection.test.js +139 -36
  58. package/dist/extension-connection.test.js.map +1 -1
  59. package/dist/ffmpeg.d.ts +148 -0
  60. package/dist/ffmpeg.d.ts.map +1 -0
  61. package/dist/ffmpeg.js +523 -0
  62. package/dist/ffmpeg.js.map +1 -0
  63. package/dist/ghost-browser.d.ts.map +1 -1
  64. package/dist/ghost-browser.js.map +1 -1
  65. package/dist/ghost-cursor-client.js +287 -0
  66. package/dist/ghost-cursor.d.ts +27 -0
  67. package/dist/ghost-cursor.d.ts.map +1 -0
  68. package/dist/ghost-cursor.js +63 -0
  69. package/dist/ghost-cursor.js.map +1 -0
  70. package/dist/htmlrewrite.d.ts.map +1 -1
  71. package/dist/htmlrewrite.js +17 -55
  72. package/dist/htmlrewrite.js.map +1 -1
  73. package/dist/htmlrewrite.test.js.map +1 -1
  74. package/dist/kill-port.d.ts.map +1 -1
  75. package/dist/kill-port.js +1 -3
  76. package/dist/kill-port.js.map +1 -1
  77. package/dist/locator-selector.test.d.ts +2 -0
  78. package/dist/locator-selector.test.d.ts.map +1 -0
  79. package/dist/locator-selector.test.js +96 -0
  80. package/dist/locator-selector.test.js.map +1 -0
  81. package/dist/mcp-client.js.map +1 -1
  82. package/dist/mcp.d.ts.map +1 -1
  83. package/dist/mcp.js +8 -3
  84. package/dist/mcp.js.map +1 -1
  85. package/dist/on-mouse-action.test.d.ts +2 -0
  86. package/dist/on-mouse-action.test.d.ts.map +1 -0
  87. package/dist/on-mouse-action.test.js +155 -0
  88. package/dist/on-mouse-action.test.js.map +1 -0
  89. package/dist/page-markdown.js +4 -4
  90. package/dist/page-markdown.js.map +1 -1
  91. package/dist/prompt.md +450 -377
  92. package/dist/protocol.d.ts +4 -0
  93. package/dist/protocol.d.ts.map +1 -1
  94. package/dist/readability.js +16 -2
  95. package/dist/recording-ghost-cursor.d.ts +41 -0
  96. package/dist/recording-ghost-cursor.d.ts.map +1 -0
  97. package/dist/recording-ghost-cursor.js +79 -0
  98. package/dist/recording-ghost-cursor.js.map +1 -0
  99. package/dist/recording-relay.d.ts.map +1 -1
  100. package/dist/recording-relay.js +8 -8
  101. package/dist/recording-relay.js.map +1 -1
  102. package/dist/relay-client.d.ts +17 -4
  103. package/dist/relay-client.d.ts.map +1 -1
  104. package/dist/relay-client.js +45 -11
  105. package/dist/relay-client.js.map +1 -1
  106. package/dist/relay-core.test.d.ts.map +1 -1
  107. package/dist/relay-core.test.js +515 -26
  108. package/dist/relay-core.test.js.map +1 -1
  109. package/dist/relay-navigation.test.d.ts.map +1 -1
  110. package/dist/relay-navigation.test.js +169 -31
  111. package/dist/relay-navigation.test.js.map +1 -1
  112. package/dist/relay-session.test.d.ts.map +1 -1
  113. package/dist/relay-session.test.js +113 -65
  114. package/dist/relay-session.test.js.map +1 -1
  115. package/dist/relay-state.d.ts +158 -0
  116. package/dist/relay-state.d.ts.map +1 -0
  117. package/dist/relay-state.js +306 -0
  118. package/dist/relay-state.js.map +1 -0
  119. package/dist/relay-state.test.d.ts +2 -0
  120. package/dist/relay-state.test.d.ts.map +1 -0
  121. package/dist/relay-state.test.js +472 -0
  122. package/dist/relay-state.test.js.map +1 -0
  123. package/dist/scoped-fs.d.ts.map +1 -1
  124. package/dist/scoped-fs.js.map +1 -1
  125. package/dist/screen-recording.d.ts +66 -4
  126. package/dist/screen-recording.d.ts.map +1 -1
  127. package/dist/screen-recording.js +150 -13
  128. package/dist/screen-recording.js.map +1 -1
  129. package/dist/screen-recording.test.d.ts +2 -0
  130. package/dist/screen-recording.test.d.ts.map +1 -0
  131. package/dist/screen-recording.test.js +102 -0
  132. package/dist/screen-recording.test.js.map +1 -0
  133. package/dist/selector-generator.js +1 -1
  134. package/dist/snapshot-tools.test.js +71 -28
  135. package/dist/snapshot-tools.test.js.map +1 -1
  136. package/dist/start-relay-server.d.ts +1 -1
  137. package/dist/start-relay-server.d.ts.map +1 -1
  138. package/dist/start-relay-server.js +1 -1
  139. package/dist/start-relay-server.js.map +1 -1
  140. package/dist/styles-api.md +8 -1
  141. package/dist/styles-examples.d.ts +1 -1
  142. package/dist/styles-examples.d.ts.map +1 -1
  143. package/dist/styles-examples.js +1 -1
  144. package/dist/styles-examples.js.map +1 -1
  145. package/dist/styles.d.ts.map +1 -1
  146. package/dist/styles.js +1 -3
  147. package/dist/styles.js.map +1 -1
  148. package/dist/test-declarations.d.ts.map +1 -1
  149. package/dist/test-utils.d.ts +1 -1
  150. package/dist/test-utils.d.ts.map +1 -1
  151. package/dist/test-utils.js +7 -5
  152. package/dist/test-utils.js.map +1 -1
  153. package/dist/utils.d.ts.map +1 -1
  154. package/dist/utils.js.map +1 -1
  155. package/dist/wait-for-page-load.d.ts.map +1 -1
  156. package/dist/wait-for-page-load.js +1 -1
  157. package/dist/wait-for-page-load.js.map +1 -1
  158. package/package.json +4 -3
  159. package/src/a11y-client.ts +5 -4
  160. package/src/aria-snapshot.test.ts +5 -2
  161. package/src/aria-snapshot.ts +306 -117
  162. package/src/aria-snapshot.unit.test.ts +199 -141
  163. package/src/aria-snapshots/github-interactive.txt +2 -0
  164. package/src/aria-snapshots/github-raw.txt +5 -1
  165. package/src/aria-snapshots/hackernews-interactive.txt +238 -241
  166. package/src/aria-snapshots/hackernews-raw.txt +265 -269
  167. package/src/assets/aria-labels-example.png +0 -0
  168. package/src/assets/aria-labels-github.png +0 -0
  169. package/src/assets/aria-labels-hacker-news.png +0 -0
  170. package/src/assets/aria-labels-old-reddit.png +0 -0
  171. package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
  172. package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
  173. package/src/cdp-log.ts +4 -1
  174. package/src/cdp-relay.ts +1059 -737
  175. package/src/cdp-session.ts +12 -3
  176. package/src/cdp-types.ts +51 -51
  177. package/src/clean-html.ts +4 -5
  178. package/src/cli.ts +82 -55
  179. package/src/create-logger.ts +5 -3
  180. package/src/debugger-examples-types.ts +4 -1
  181. package/src/debugger.ts +1 -5
  182. package/src/diff-utils.ts +2 -5
  183. package/src/editor-examples.ts +11 -1
  184. package/src/editor.ts +10 -2
  185. package/src/executor.ts +374 -73
  186. package/src/executor.unit.test.ts +48 -1
  187. package/src/extension-connection.test.ts +612 -488
  188. package/src/ffmpeg.ts +769 -0
  189. package/src/ghost-browser.ts +4 -6
  190. package/src/ghost-cursor-client.ts +369 -0
  191. package/src/ghost-cursor.ts +110 -0
  192. package/src/htmlrewrite.test.ts +6 -2
  193. package/src/htmlrewrite.ts +348 -386
  194. package/src/kill-port.ts +1 -3
  195. package/src/locator-selector.test.ts +115 -0
  196. package/src/mcp-client.ts +1 -1
  197. package/src/mcp.ts +21 -15
  198. package/src/on-mouse-action.test.ts +196 -0
  199. package/src/page-markdown.ts +7 -7
  200. package/src/protocol.ts +73 -57
  201. package/src/recording-ghost-cursor.ts +113 -0
  202. package/src/recording-relay.ts +20 -12
  203. package/src/relay-client.ts +85 -18
  204. package/src/relay-core.test.ts +1117 -578
  205. package/src/relay-navigation.test.ts +648 -483
  206. package/src/relay-session.test.ts +984 -929
  207. package/src/relay-state.test.ts +570 -0
  208. package/src/relay-state.ts +497 -0
  209. package/src/resource.md +21 -49
  210. package/src/scoped-fs.ts +9 -3
  211. package/src/screen-recording.test.ts +111 -0
  212. package/src/screen-recording.ts +256 -31
  213. package/src/skill.md +476 -396
  214. package/src/snapshot-tools.test.ts +580 -528
  215. package/src/snapshots/shadcn-ui-accessibility-full.md +8 -8
  216. package/src/snapshots/shadcn-ui-accessibility-interactive.md +8 -8
  217. package/src/start-relay-server.ts +14 -11
  218. package/src/styles-examples.ts +8 -1
  219. package/src/styles.ts +20 -21
  220. package/src/test-declarations.ts +6 -6
  221. package/src/test-utils.ts +104 -91
  222. package/src/utils.ts +2 -1
  223. package/src/wait-for-page-load.ts +6 -1
@@ -14,9 +14,15 @@ export interface ICDPSession {
14
14
  sessionId?: string | null,
15
15
  ): Promise<ProtocolMapping.Commands[K]['returnType']>
16
16
 
17
- on<K extends keyof ProtocolMapping.Events>(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): unknown
17
+ on<K extends keyof ProtocolMapping.Events>(
18
+ event: K,
19
+ callback: (params: ProtocolMapping.Events[K][0]) => void,
20
+ ): unknown
18
21
 
19
- off<K extends keyof ProtocolMapping.Events>(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): unknown
22
+ off<K extends keyof ProtocolMapping.Events>(
23
+ event: K,
24
+ callback: (params: ProtocolMapping.Events[K][0]) => void,
25
+ ): unknown
20
26
 
21
27
  detach(): Promise<void>
22
28
  getSessionId?(): string | null
@@ -46,7 +52,10 @@ export class PlaywrightCDPSessionAdapter implements ICDPSession {
46
52
  return this
47
53
  }
48
54
 
49
- off<K extends keyof ProtocolMapping.Events>(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): this {
55
+ off<K extends keyof ProtocolMapping.Events>(
56
+ event: K,
57
+ callback: (params: ProtocolMapping.Events[K][0]) => void,
58
+ ): this {
50
59
  this._playwrightSession.off(event as never, callback as never)
51
60
  return this
52
61
  }
package/src/cdp-types.ts CHANGED
@@ -1,55 +1,55 @@
1
- import type { Protocol } from 'devtools-protocol';
2
- import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js';
1
+ import type { Protocol } from 'devtools-protocol'
2
+ import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js'
3
3
 
4
- export type CDPCommandSource = 'playwriter';
4
+ export type CDPCommandSource = 'playwriter'
5
5
 
6
6
  export type CDPCommandFor<T extends keyof ProtocolMapping.Commands> = {
7
- id: number;
8
- sessionId?: string;
9
- method: T;
10
- params?: ProtocolMapping.Commands[T]['paramsType'][0];
11
- source?: CDPCommandSource;
12
- };
7
+ id: number
8
+ sessionId?: string
9
+ method: T
10
+ params?: ProtocolMapping.Commands[T]['paramsType'][0]
11
+ source?: CDPCommandSource
12
+ }
13
13
 
14
14
  export type CDPCommand = {
15
- [K in keyof ProtocolMapping.Commands]: CDPCommandFor<K>;
16
- }[keyof ProtocolMapping.Commands];
15
+ [K in keyof ProtocolMapping.Commands]: CDPCommandFor<K>
16
+ }[keyof ProtocolMapping.Commands]
17
17
 
18
18
  export type CDPResponseFor<T extends keyof ProtocolMapping.Commands> = {
19
- id: number;
20
- sessionId?: string;
21
- result?: ProtocolMapping.Commands[T]['returnType'];
22
- error?: { code?: number; message: string };
23
- };
19
+ id: number
20
+ sessionId?: string
21
+ result?: ProtocolMapping.Commands[T]['returnType']
22
+ error?: { code?: number; message: string }
23
+ }
24
24
 
25
25
  export type CDPResponse = {
26
- [K in keyof ProtocolMapping.Commands]: CDPResponseFor<K>;
27
- }[keyof ProtocolMapping.Commands];
26
+ [K in keyof ProtocolMapping.Commands]: CDPResponseFor<K>
27
+ }[keyof ProtocolMapping.Commands]
28
28
 
29
29
  export type CDPEventFor<T extends keyof ProtocolMapping.Events> = {
30
- method: T;
31
- sessionId?: string;
32
- params?: ProtocolMapping.Events[T][0];
33
- };
30
+ method: T
31
+ sessionId?: string
32
+ params?: ProtocolMapping.Events[T][0]
33
+ }
34
34
 
35
35
  export type CDPEvent = {
36
- [K in keyof ProtocolMapping.Events]: CDPEventFor<K>;
37
- }[keyof ProtocolMapping.Events];
36
+ [K in keyof ProtocolMapping.Events]: CDPEventFor<K>
37
+ }[keyof ProtocolMapping.Events]
38
38
 
39
39
  export type CDPResponseBase = {
40
- id: number;
41
- sessionId?: string;
42
- result?: unknown;
43
- error?: { code?: number; message: string };
44
- };
40
+ id: number
41
+ sessionId?: string
42
+ result?: unknown
43
+ error?: { code?: number; message: string }
44
+ }
45
45
 
46
46
  export type CDPEventBase = {
47
- method: string;
48
- sessionId?: string;
49
- params?: unknown;
50
- };
47
+ method: string
48
+ sessionId?: string
49
+ params?: unknown
50
+ }
51
51
 
52
- export type CDPMessage = CDPCommand | CDPResponse | CDPEvent;
52
+ export type CDPMessage = CDPCommand | CDPResponse | CDPEvent
53
53
 
54
54
  export type RelayServerEvents = {
55
55
  'cdp:command': (data: { clientId: string; command: CDPCommand }) => void
@@ -57,14 +57,14 @@ export type RelayServerEvents = {
57
57
  'cdp:response': (data: { clientId: string; response: CDPResponseBase; command: CDPCommand }) => void
58
58
  }
59
59
 
60
- export { Protocol, ProtocolMapping };
60
+ export { Protocol, ProtocolMapping }
61
61
 
62
62
  // types tests. to see if types are right with some simple examples
63
63
  if (false as any) {
64
64
  const browserVersionCommand = {
65
65
  id: 1,
66
66
  method: 'Browser.getVersion',
67
- } satisfies CDPCommand;
67
+ } satisfies CDPCommand
68
68
 
69
69
  const browserVersionResponse = {
70
70
  id: 1,
@@ -74,8 +74,8 @@ if (false as any) {
74
74
  revision: '123',
75
75
  userAgent: 'Mozilla/5.0',
76
76
  jsVersion: 'V8',
77
- }
78
- } satisfies CDPResponse;
77
+ },
78
+ } satisfies CDPResponse
79
79
 
80
80
  const targetAttachCommand = {
81
81
  id: 2,
@@ -83,13 +83,13 @@ if (false as any) {
83
83
  params: {
84
84
  autoAttach: true,
85
85
  waitForDebuggerOnStart: false,
86
- }
87
- } satisfies CDPCommand;
86
+ },
87
+ } satisfies CDPCommand
88
88
 
89
89
  const targetAttachResponse = {
90
90
  id: 2,
91
91
  result: undefined,
92
- } satisfies CDPResponse;
92
+ } satisfies CDPResponse
93
93
 
94
94
  const attachedToTargetEvent = {
95
95
  method: 'Target.attachedToTarget',
@@ -104,8 +104,8 @@ if (false as any) {
104
104
  canAccessOpener: false,
105
105
  },
106
106
  waitingForDebugger: false,
107
- }
108
- } satisfies CDPEvent;
107
+ },
108
+ } satisfies CDPEvent
109
109
 
110
110
  const consoleMessageEvent = {
111
111
  method: 'Runtime.consoleAPICalled',
@@ -114,23 +114,23 @@ if (false as any) {
114
114
  args: [],
115
115
  executionContextId: 1,
116
116
  timestamp: 123456789,
117
- }
118
- } satisfies CDPEvent;
117
+ },
118
+ } satisfies CDPEvent
119
119
 
120
120
  const pageNavigateCommand = {
121
121
  id: 3,
122
122
  method: 'Page.navigate',
123
123
  params: {
124
124
  url: 'https://example.com',
125
- }
126
- } satisfies CDPCommand;
125
+ },
126
+ } satisfies CDPCommand
127
127
 
128
128
  const pageNavigateResponse = {
129
129
  id: 3,
130
130
  result: {
131
131
  frameId: 'frame-1',
132
- }
133
- } satisfies CDPResponse;
132
+ },
133
+ } satisfies CDPResponse
134
134
 
135
135
  const networkRequestEvent = {
136
136
  method: 'Network.requestWillBeSent',
@@ -153,6 +153,6 @@ if (false as any) {
153
153
  },
154
154
  redirectHasExtraInfo: false,
155
155
  type: 'XHR',
156
- }
157
- } satisfies CDPEvent;
156
+ },
157
+ } satisfies CDPEvent
158
158
  }
package/src/clean-html.ts CHANGED
@@ -26,17 +26,16 @@ function isRegExp(value: any): value is RegExp {
26
26
 
27
27
  function getSnapshotKey(locator: Locator | Page): string {
28
28
  if (isPage(locator)) {
29
- return '__page__'
29
+ return 'page'
30
30
  }
31
- // For locators, use a string representation
32
- return (locator as any)._selector || '__locator__'
31
+ return `locator:${locator.selector()}`
33
32
  }
34
33
 
35
34
  export async function getCleanHTML(options: GetCleanHTMLOptions): Promise<string> {
36
35
  const {
37
36
  locator,
38
37
  search,
39
- showDiffSinceLastCall = true,
38
+ showDiffSinceLastCall = !search,
40
39
  includeStyles = false,
41
40
  maxAttrLen = 200,
42
41
  maxContentLen = 500,
@@ -76,7 +75,7 @@ export async function getCleanHTML(options: GetCleanHTMLOptions): Promise<string
76
75
  const previousSnapshot = pageSnapshots.get(snapshotKey)
77
76
  pageSnapshots.set(snapshotKey, htmlStr)
78
77
 
79
- // Return diff if we have a previous snapshot and diff mode is enabled
78
+ // Diff defaults off when search is provided, but agent can explicitly enable both
80
79
  if (showDiffSinceLastCall && previousSnapshot) {
81
80
  const diffResult = createSmartDiff({
82
81
  oldContent: previousSnapshot,
package/src/cli.ts CHANGED
@@ -5,6 +5,7 @@ import path from 'node:path'
5
5
  import util from 'node:util'
6
6
  import { fileURLToPath } from 'node:url'
7
7
  import { cac } from '@xmorse/cac'
8
+ import pc from 'picocolors'
8
9
 
9
10
  // Prevent Buffers from dumping hex bytes in util.inspect output.
10
11
  Buffer.prototype[util.inspect.custom] = function () {
@@ -12,7 +13,14 @@ Buffer.prototype[util.inspect.custom] = function () {
12
13
  }
13
14
  import { killPortProcess } from './kill-port.js'
14
15
  import { VERSION, LOG_FILE_PATH, LOG_CDP_FILE_PATH, parseRelayHost } from './utils.js'
15
- import { ensureRelayServer, RELAY_PORT, waitForExtension, getExtensionOutdatedWarning, getExtensionStatus } from './relay-client.js'
16
+ import {
17
+ ensureRelayServer,
18
+ RELAY_PORT,
19
+ waitForConnectedExtensions,
20
+ getExtensionOutdatedWarning,
21
+ getExtensionStatus,
22
+ type ExtensionStatus,
23
+ } from './relay-client.js'
16
24
 
17
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
18
26
 
@@ -20,15 +28,6 @@ const cliRelayEnv = { PLAYWRITER_AUTO_ENABLE: '1' }
20
28
 
21
29
  const cli = cac('playwriter')
22
30
 
23
- type ExtensionStatus = {
24
- extensionId: string
25
- stableKey?: string
26
- browser: string | null
27
- profile: { email: string; id: string } | null
28
- activeTargets: number
29
- playwriterVersion?: string | null
30
- }
31
-
32
31
  cli
33
32
  .command('', 'Start the MCP server or controls the browser with -e')
34
33
  .option('--host <host>', 'Remote relay server host to connect to (or use PLAYWRITER_HOST env var)')
@@ -76,7 +75,7 @@ async function fetchExtensionsStatus(host?: string): Promise<ExtensionStatus[]>
76
75
  if (!fallback.ok) {
77
76
  return []
78
77
  }
79
- const fallbackData = await fallback.json() as {
78
+ const fallbackData = (await fallback.json()) as {
80
79
  connected: boolean
81
80
  activeTargets: number
82
81
  browser: string | null
@@ -86,16 +85,18 @@ async function fetchExtensionsStatus(host?: string): Promise<ExtensionStatus[]>
86
85
  if (!fallbackData?.connected) {
87
86
  return []
88
87
  }
89
- return [{
90
- extensionId: 'default',
91
- stableKey: undefined,
92
- browser: fallbackData?.browser,
93
- profile: fallbackData?.profile,
94
- activeTargets: fallbackData?.activeTargets,
95
- playwriterVersion: fallbackData?.playwriterVersion || null,
96
- }]
88
+ return [
89
+ {
90
+ extensionId: 'default',
91
+ stableKey: undefined,
92
+ browser: fallbackData?.browser,
93
+ profile: fallbackData?.profile,
94
+ activeTargets: fallbackData?.activeTargets,
95
+ playwriterVersion: fallbackData?.playwriterVersion || null,
96
+ },
97
+ ]
97
98
  }
98
- const data = await response.json() as {
99
+ const data = (await response.json()) as {
99
100
  extensions: ExtensionStatus[]
100
101
  }
101
102
  return data?.extensions || []
@@ -128,8 +129,12 @@ async function executeCode(options: {
128
129
  if (!host && !process.env.PLAYWRITER_HOST) {
129
130
  const restarted = await ensureRelayServer({ logger: console, env: cliRelayEnv })
130
131
  if (restarted) {
131
- const connected = await waitForExtension({ logger: console, timeoutMs: 10000 })
132
- if (!connected) {
132
+ const connectedExtensions = await waitForConnectedExtensions({
133
+ logger: console,
134
+ timeoutMs: 10000,
135
+ pollIntervalMs: 250,
136
+ })
137
+ if (connectedExtensions.length === 0) {
133
138
  console.error('Warning: Extension not connected. Commands may fail.')
134
139
  }
135
140
  }
@@ -150,7 +155,9 @@ async function executeCode(options: {
150
155
  method: 'POST',
151
156
  headers: {
152
157
  'Content-Type': 'application/json',
153
- ...(token || process.env.PLAYWRITER_TOKEN ? { 'Authorization': `Bearer ${token || process.env.PLAYWRITER_TOKEN}` } : {}),
158
+ ...(token || process.env.PLAYWRITER_TOKEN
159
+ ? { Authorization: `Bearer ${token || process.env.PLAYWRITER_TOKEN}` }
160
+ : {}),
154
161
  },
155
162
  body: JSON.stringify({ sessionId, code, timeout, cwd }),
156
163
  })
@@ -161,7 +168,11 @@ async function executeCode(options: {
161
168
  process.exit(1)
162
169
  }
163
170
 
164
- const result = await response.json() as { text: string; images: Array<{ data: string; mimeType: string }>; isError: boolean }
171
+ const result = (await response.json()) as {
172
+ text: string
173
+ images: Array<{ data: string; mimeType: string }>
174
+ isError: boolean
175
+ }
165
176
 
166
177
  // Print output
167
178
  if (result.text) {
@@ -198,16 +209,30 @@ cli
198
209
  .option('--host <host>', 'Remote relay server host')
199
210
  .option('--browser <stableKey>', 'Stable browser key when multiple browsers are connected')
200
211
  .action(async (options: { host?: string; browser?: string }) => {
201
- if (!options.host && !process.env.PLAYWRITER_HOST) {
212
+ const isLocal = !options.host && !process.env.PLAYWRITER_HOST
213
+
214
+ let extensions: ExtensionStatus[] = []
215
+
216
+ if (isLocal) {
202
217
  await ensureRelayServer({ logger: console, env: cliRelayEnv })
203
- await waitForExtension({
204
- timeoutMs: 3000,
218
+ extensions = await waitForConnectedExtensions({
219
+ timeoutMs: 12000,
220
+ pollIntervalMs: 250,
205
221
  logger: console,
206
222
  })
207
- }
208
223
 
224
+ if (extensions.length === 0) {
225
+ console.log(pc.dim('Waiting briefly for extension to reconnect...'))
226
+ extensions = await waitForConnectedExtensions({
227
+ timeoutMs: 10000,
228
+ pollIntervalMs: 250,
229
+ logger: console,
230
+ })
231
+ }
232
+ } else {
233
+ extensions = await fetchExtensionsStatus(options.host)
234
+ }
209
235
 
210
- const extensions = await fetchExtensionsStatus(options.host)
211
236
  if (extensions.length === 0) {
212
237
  console.error('No connected browsers detected. Click the Playwriter extension icon.')
213
238
  process.exit(1)
@@ -253,9 +278,10 @@ cli
253
278
 
254
279
  try {
255
280
  const serverUrl = await getServerUrl(options.host)
256
- const extensionId = selectedExtension.extensionId === 'default'
257
- ? null
258
- : (selectedExtension.stableKey || selectedExtension.extensionId)
281
+ const extensionId =
282
+ selectedExtension.extensionId === 'default'
283
+ ? null
284
+ : selectedExtension.stableKey || selectedExtension.extensionId
259
285
  const cwd = process.cwd()
260
286
  const response = await fetch(`${serverUrl}/cli/session/new`, {
261
287
  method: 'POST',
@@ -267,7 +293,7 @@ cli
267
293
  console.error(`Error: ${response.status} ${text}`)
268
294
  process.exit(1)
269
295
  }
270
- const result = await response.json() as { id: string; extensionId: string | null }
296
+ const result = (await response.json()) as { id: string; extensionId: string | null }
271
297
  console.log(`Session ${result.id} created. Use with: playwriter -s ${result.id} -e "..."`)
272
298
  } catch (error: any) {
273
299
  console.error(`Error: ${error.message}`)
@@ -300,7 +326,7 @@ cli
300
326
  console.error(`Error: ${response.status} ${await response.text()}`)
301
327
  process.exit(1)
302
328
  }
303
- const result = await response.json() as {
329
+ const result = (await response.json()) as {
304
330
  sessions: Array<{
305
331
  id: string
306
332
  stateKeys: string[]
@@ -335,7 +361,7 @@ cli
335
361
  ' ' +
336
362
  'EXT'.padEnd(extensionWidth) +
337
363
  ' ' +
338
- 'STATE KEYS'
364
+ 'STATE KEYS',
339
365
  )
340
366
  console.log('-'.repeat(idWidth + browserWidth + profileWidth + extensionWidth + stateWidth + 8))
341
367
 
@@ -351,7 +377,7 @@ cli
351
377
  ' ' +
352
378
  (session.extensionId || '-').padEnd(extensionWidth) +
353
379
  ' ' +
354
- stateStr
380
+ stateStr,
355
381
  )
356
382
  }
357
383
  })
@@ -374,7 +400,7 @@ cli
374
400
  })
375
401
 
376
402
  if (!response.ok) {
377
- const result = await response.json() as { error: string }
403
+ const result = (await response.json()) as { error: string }
378
404
  console.error(`Error: ${result.error}`)
379
405
  process.exit(1)
380
406
  }
@@ -410,8 +436,10 @@ cli
410
436
  process.exit(1)
411
437
  }
412
438
 
413
- const result = await response.json() as { success: boolean; pageUrl: string; pagesCount: number }
414
- console.log(`Connection reset successfully. ${result.pagesCount} page(s) available. Current page URL: ${result.pageUrl}`)
439
+ const result = (await response.json()) as { success: boolean; pageUrl: string; pagesCount: number }
440
+ console.log(
441
+ `Connection reset successfully. ${result.pagesCount} page(s) available. Current page URL: ${result.pageUrl}`,
442
+ )
415
443
  } catch (error: any) {
416
444
  console.error(`Error: ${error.message}`)
417
445
  process.exit(1)
@@ -419,9 +447,12 @@ cli
419
447
  })
420
448
 
421
449
  cli
422
- .command('serve', 'Start the CDP relay server for remote MCP connections')
423
- .option('--host <host>', 'Host to bind to', { default: '0.0.0.0' })
424
- .option('--token <token>', 'Authentication token (or use PLAYWRITER_TOKEN env var)')
450
+ .command(
451
+ 'serve',
452
+ `Start the relay server on this machine (must be the same host where Chrome is running). Remote clients (Docker, other machines) connect via PLAYWRITER_HOST. Use --host localhost for Docker (no token needed) containers reach it via host.docker.internal. Use --host 0.0.0.0 for LAN/internet access (requires --token).`,
453
+ )
454
+ .option('--host <host>', 'Host to bind to (use "localhost" for Docker, "0.0.0.0" for remote access)', { default: '0.0.0.0' })
455
+ .option('--token <token>', 'Authentication token, required when --host is 0.0.0.0 (or use PLAYWRITER_TOKEN env var)')
425
456
  .option('--replace', 'Kill existing server if running')
426
457
  .action(async (options: { host: string; token?: string; replace?: boolean }) => {
427
458
  const token = options.token || process.env.PLAYWRITER_TOKEN
@@ -512,20 +543,16 @@ cli
512
543
  })
513
544
  })
514
545
 
515
- cli
516
- .command('logfile', 'Print the path to the relay server log file')
517
- .action(() => {
518
- console.log(`relay: ${LOG_FILE_PATH}`)
519
- console.log(`cdp: ${LOG_CDP_FILE_PATH}`)
520
- })
546
+ cli.command('logfile', 'Print the path to the relay server log file').action(() => {
547
+ console.log(`relay: ${LOG_FILE_PATH}`)
548
+ console.log(`cdp: ${LOG_CDP_FILE_PATH}`)
549
+ })
521
550
 
522
- cli
523
- .command('skill', 'Print the full playwriter usage instructions')
524
- .action(() => {
525
- const skillPath = path.join(__dirname, '..', 'src', 'skill.md')
526
- const content = fs.readFileSync(skillPath, 'utf-8')
527
- console.log(content)
528
- })
551
+ cli.command('skill', 'Print the full playwriter usage instructions').action(() => {
552
+ const skillPath = path.join(__dirname, '..', 'src', 'skill.md')
553
+ const content = fs.readFileSync(skillPath, 'utf-8')
554
+ console.log(content)
555
+ })
529
556
 
530
557
  cli.help()
531
558
  cli.version(VERSION)
@@ -21,9 +21,11 @@ export function createFileLogger({ logFilePath }: { logFilePath?: string } = {})
21
21
  let queue: Promise<void> = Promise.resolve()
22
22
 
23
23
  const log = (...args: unknown[]): Promise<void> => {
24
- const message = args.map(arg =>
25
- typeof arg === 'string' ? arg : util.inspect(arg, { depth: null, colors: false, maxStringLength: 1000 })
26
- ).join(' ')
24
+ const message = args
25
+ .map((arg) =>
26
+ typeof arg === 'string' ? arg : util.inspect(arg, { depth: null, colors: false, maxStringLength: 1000 }),
27
+ )
28
+ .join(' ')
27
29
  queue = queue.then(() => fs.promises.appendFile(resolvedLogFilePath, stripAnsi(message) + '\n'))
28
30
  return queue
29
31
  }
@@ -8,6 +8,9 @@ export declare const page: Page
8
8
  export declare const getCDPSession: (options: { page: Page }) => Promise<ICDPSession>
9
9
  export declare const createDebugger: (options: { cdp: ICDPSession }) => Debugger
10
10
  export declare const createEditor: (options: { cdp: ICDPSession }) => Editor
11
- export declare const getStylesForLocator: (options: { locator: Locator; includeUserAgentStyles?: boolean }) => Promise<StylesResult>
11
+ export declare const getStylesForLocator: (options: {
12
+ locator: Locator
13
+ includeUserAgentStyles?: boolean
14
+ }) => Promise<StylesResult>
12
15
  export declare const formatStylesAsText: (styles: StylesResult) => string
13
16
  export declare const console: { log: (...args: unknown[]) => void }
package/src/debugger.ts CHANGED
@@ -24,8 +24,6 @@ export interface EvaluateResult {
24
24
  value: unknown
25
25
  }
26
26
 
27
-
28
-
29
27
  export interface ScriptInfo {
30
28
  scriptId: string
31
29
  url: string
@@ -533,9 +531,7 @@ export class Debugger {
533
531
  async listScripts({ search }: { search?: string } = {}): Promise<ScriptInfo[]> {
534
532
  await this.enable()
535
533
  const scripts = Array.from(this.scripts.values())
536
- const filtered = search
537
- ? scripts.filter((s) => s.url.toLowerCase().includes(search.toLowerCase()))
538
- : scripts
534
+ const filtered = search ? scripts.filter((s) => s.url.toLowerCase().includes(search.toLowerCase())) : scripts
539
535
  return filtered.slice(0, 20)
540
536
  }
541
537
 
package/src/diff-utils.ts CHANGED
@@ -16,7 +16,7 @@ export interface CreateSmartDiffOptions {
16
16
 
17
17
  /**
18
18
  * Creates a smart diff that returns full content when changes exceed threshold.
19
- *
19
+ *
20
20
  * When more than `threshold` (default 50%) of lines have changed, showing a diff
21
21
  * is not useful - we return the full new content instead.
22
22
  */
@@ -54,10 +54,7 @@ export function createSmartDiff(options: CreateSmartDiffOptions): SmartDiffResul
54
54
  const changeRatio = Math.min(changedLines / maxLines, 1) // Cap at 100%
55
55
 
56
56
  // Build unified diff string from structured patch
57
- const diffLines: string[] = [
58
- `--- ${label} (previous)`,
59
- `+++ ${label} (current)`,
60
- ]
57
+ const diffLines: string[] = [`--- ${label} (previous)`, `+++ ${label} (current)`]
61
58
  for (const hunk of patch.hunks) {
62
59
  diffLines.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`)
63
60
  diffLines.push(...hunk.lines)
@@ -145,4 +145,14 @@ async function searchStyles() {
145
145
  console.log(matches)
146
146
  }
147
147
 
148
- export { listScripts, readScript, editScript, searchScripts, writeScript, editInlineScript, readStylesheet, editStylesheet, searchStyles }
148
+ export {
149
+ listScripts,
150
+ readScript,
151
+ editScript,
152
+ searchScripts,
153
+ writeScript,
154
+ editInlineScript,
155
+ readStylesheet,
156
+ editStylesheet,
157
+ searchStyles,
158
+ }
package/src/editor.ts CHANGED
@@ -294,7 +294,7 @@ export class Editor {
294
294
  private async setSource(
295
295
  id: { scriptId: string } | { styleSheetId: string },
296
296
  content: string,
297
- dryRun = false
297
+ dryRun = false,
298
298
  ): Promise<EditResult> {
299
299
  if ('styleSheetId' in id) {
300
300
  await this.cdp.send('CSS.setStyleSheetText', { styleSheetId: id.styleSheetId, text: content })
@@ -410,7 +410,15 @@ export class Editor {
410
410
  * @param options.content - New content
411
411
  * @param options.dryRun - If true, validate without applying (default false, only works for JS)
412
412
  */
413
- async write({ url, content, dryRun = false }: { url: string; content: string; dryRun?: boolean }): Promise<EditResult> {
413
+ async write({
414
+ url,
415
+ content,
416
+ dryRun = false,
417
+ }: {
418
+ url: string
419
+ content: string
420
+ dryRun?: boolean
421
+ }): Promise<EditResult> {
414
422
  await this.enable()
415
423
  const id = this.getIdByUrl(url)
416
424
  return this.setSource(id, content, dryRun)