playwriter 0.2.0 → 0.3.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/dist/bippy.js +1 -1
  2. package/dist/cdp-log.d.ts +4 -1
  3. package/dist/cdp-log.d.ts.map +1 -1
  4. package/dist/cdp-log.js +39 -2
  5. package/dist/cdp-log.js.map +1 -1
  6. package/dist/cdp-log.test.d.ts +2 -0
  7. package/dist/cdp-log.test.d.ts.map +1 -0
  8. package/dist/cdp-log.test.js +109 -0
  9. package/dist/cdp-log.test.js.map +1 -0
  10. package/dist/cdp-relay.d.ts.map +1 -1
  11. package/dist/cdp-relay.js +99 -6
  12. package/dist/cdp-relay.js.map +1 -1
  13. package/dist/cli.js +14 -12
  14. package/dist/cli.js.map +1 -1
  15. package/dist/executor.d.ts +3 -0
  16. package/dist/executor.d.ts.map +1 -1
  17. package/dist/executor.js +106 -36
  18. package/dist/executor.js.map +1 -1
  19. package/dist/extension/background.js +23 -12
  20. package/dist/extension/manifest.json +1 -1
  21. package/dist/prompt.md +32 -13
  22. package/dist/readability.js +1 -1
  23. package/dist/relay-client.d.ts +11 -0
  24. package/dist/relay-client.d.ts.map +1 -1
  25. package/dist/relay-client.js +46 -1
  26. package/dist/relay-client.js.map +1 -1
  27. package/dist/relay-core.test.js +10 -6
  28. package/dist/relay-core.test.js.map +1 -1
  29. package/dist/relay-session.test.js +9 -1
  30. package/dist/relay-session.test.js.map +1 -1
  31. package/dist/relay-state.test.js +57 -1
  32. package/dist/relay-state.test.js.map +1 -1
  33. package/dist/selector-generator.js +1 -1
  34. package/dist/start-relay-server.d.ts +1 -1
  35. package/dist/start-relay-server.d.ts.map +1 -1
  36. package/dist/start-relay-server.js +23 -1
  37. package/dist/start-relay-server.js.map +1 -1
  38. package/dist/utils.d.ts +1 -0
  39. package/dist/utils.d.ts.map +1 -1
  40. package/dist/utils.js +3 -0
  41. package/dist/utils.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/cdp-log.test.ts +131 -0
  44. package/src/cdp-log.ts +44 -2
  45. package/src/cdp-relay.ts +104 -6
  46. package/src/cli.ts +14 -13
  47. package/src/executor.ts +122 -39
  48. package/src/relay-client.ts +62 -5
  49. package/src/relay-core.test.ts +10 -6
  50. package/src/relay-session.test.ts +9 -1
  51. package/src/relay-state.test.ts +67 -1
  52. package/src/skill.md +32 -13
  53. package/src/start-relay-server.ts +22 -1
  54. package/src/utils.ts +4 -0
@@ -1 +1 @@
1
- {"version":3,"file":"start-relay-server.js","sourceRoot":"","sources":["../src/start-relay-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE9C,OAAO,CAAC,KAAK,GAAG,sBAAsB,CAAA;AAEtC,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAA;AAEjC,OAAO,CAAC,EAAE,CAAC,mBAAmB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IAC5C,MAAM,MAAM,CAAC,KAAK,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAA;IAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA;AAEF,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;IAChD,MAAM,MAAM,CAAC,KAAK,CAAC,sBAAsB,EAAE,MAAM,CAAC,CAAA;IAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA;AAEF,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;IAChC,MAAM,MAAM,CAAC,GAAG,CAAC,8BAA8B,IAAI,EAAE,CAAC,CAAA;AACxD,CAAC,CAAC,CAAA;AAEF,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAChC,IAAI,GAAG,KAAK,EACZ,IAAI,GAAG,WAAW,EAClB,KAAK,MAC+C,EAAE;IACtD,MAAM,MAAM,GAAG,MAAM,6BAA6B,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;IAEjF,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAA;IAC9D,OAAO,CAAC,GAAG,CAAC,4BAA4B,EAAE,MAAM,CAAC,WAAW,CAAC,CAAA;IAC7D,OAAO,CAAC,GAAG,CAAC,gCAAgC,EAAE,iBAAiB,CAAC,CAAA;IAEhE,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;QACjC,MAAM,CAAC,KAAK,EAAE,CAAA;QACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAC,CAAA;IAEF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;QACjC,MAAM,CAAC,KAAK,EAAE,CAAA;QACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAC,CAAA;IAEF,OAAO,MAAM,CAAA;AACf,CAAC;AACD,WAAW,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA"}
1
+ {"version":3,"file":"start-relay-server.js","sourceRoot":"","sources":["../src/start-relay-server.ts"],"names":[],"mappings":"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"}
package/dist/utils.d.ts CHANGED
@@ -17,6 +17,7 @@ export declare function getCdpUrl({ port, host, token, extensionId, }?: {
17
17
  token?: string;
18
18
  extensionId?: string | null;
19
19
  }): string;
20
+ export declare function shouldAutoEnablePlaywriter(): boolean;
20
21
  export declare const LOG_FILE_PATH: string;
21
22
  export declare const LOG_CDP_FILE_PATH: string;
22
23
  export declare const VERSION: string;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,aAAa,UAGzB,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAc,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAY7G;AAED,wBAAgB,SAAS,CAAC,EACxB,IAAY,EACZ,IAAkB,EAClB,KAAK,EACL,WAAW,GACZ,GAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,UAaL;AAID,eAAO,MAAM,aAAa,QAAsF,CAAA;AAChH,eAAO,MAAM,iBAAiB,QACmE,CAAA;AAGjG,eAAO,MAAM,OAAO,EAAoE,MAAM,CAAA;AAE9F,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,aAAa,UAGzB,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAc,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAY7G;AAED,wBAAgB,SAAS,CAAC,EACxB,IAAY,EACZ,IAAkB,EAClB,KAAK,EACL,WAAW,GACZ,GAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,UAaL;AAED,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAID,eAAO,MAAM,aAAa,QAAsF,CAAA;AAChH,eAAO,MAAM,iBAAiB,QACmE,CAAA;AAGjG,eAAO,MAAM,OAAO,EAAoE,MAAM,CAAA;AAE9F,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C"}
package/dist/utils.js CHANGED
@@ -42,6 +42,9 @@ export function getCdpUrl({ port = 19988, host = '127.0.0.1', token, extensionId
42
42
  const { wsBaseUrl } = parseRelayHost(host, port);
43
43
  return `${wsBaseUrl}/cdp/${id}${suffix}`;
44
44
  }
45
+ export function shouldAutoEnablePlaywriter() {
46
+ return process.env.PLAYWRITER_AUTO_ENABLE?.toLowerCase() !== 'false';
47
+ }
45
48
  // Use ~/.playwriter for logs so each OS user gets their own dir (avoids permission errors on shared machines, see #44)
46
49
  const LOG_BASE_DIR = path.join(os.homedir(), '.playwriter');
47
50
  export const LOG_FILE_PATH = process.env.PLAYWRITER_LOG_FILE_PATH || path.join(LOG_BASE_DIR, 'relay-server.log');
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,kCAAkC,EAAE,gCAAgC;IACpE,kCAAkC,EAAE,8CAA8C;CACnF,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,OAAe,KAAK;IAC/D,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAA;QACzB,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,CAAA;QAC9B,MAAM,UAAU,GAAG,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAA;QAC7D,MAAM,SAAS,GAAG,GAAG,UAAU,KAAK,GAAG,CAAC,IAAI,EAAE,CAAA;QAC9C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAA;IACnC,CAAC;IACD,OAAO;QACL,WAAW,EAAE,UAAU,IAAI,IAAI,IAAI,EAAE;QACrC,SAAS,EAAE,QAAQ,IAAI,IAAI,IAAI,EAAE;KAClC,CAAA;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,EACxB,IAAI,GAAG,KAAK,EACZ,IAAI,GAAG,WAAW,EAClB,KAAK,EACL,WAAW,MAMT,EAAE;IACJ,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAA;IACzE,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAA;IACpC,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IAC5B,CAAC;IACD,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,CAAC,GAAG,CAAC,aAAa,EAAE,WAAW,CAAC,CAAA;IACxC,CAAC;IACD,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAA;IACrC,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IACnD,MAAM,EAAE,SAAS,EAAE,GAAG,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAChD,OAAO,GAAG,SAAS,QAAQ,EAAE,GAAG,MAAM,EAAE,CAAA;AAC1C,CAAC;AAED,uHAAuH;AACvH,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,aAAa,CAAC,CAAA;AAC3D,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,kBAAkB,CAAC,CAAA;AAChH,MAAM,CAAC,MAAM,iBAAiB,GAC5B,OAAO,CAAC,GAAG,CAAC,4BAA4B,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC,CAAA;AAEjG,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,CAAC,CAAA;AACrG,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC,OAAiB,CAAA;AAE9F,MAAM,UAAU,KAAK,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AAC1D,CAAC"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "playwriter",
3
3
  "description": "",
4
- "version": "0.2.0",
4
+ "version": "0.3.1",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -0,0 +1,131 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import os from 'node:os'
5
+ import { createCdpLogger, type CdpLogEntry } from './cdp-log.js'
6
+
7
+ function makeTmpDir() {
8
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'cdp-log-test-'))
9
+ }
10
+
11
+ function makeEntry(i: number): CdpLogEntry {
12
+ return {
13
+ timestamp: new Date().toISOString(),
14
+ direction: 'from-extension',
15
+ message: { method: `Test.method${i}`, id: i },
16
+ }
17
+ }
18
+
19
+ function readIds(logFile: string): number[] {
20
+ return fs
21
+ .readFileSync(logFile, 'utf-8')
22
+ .trim()
23
+ .split('\n')
24
+ .filter((l) => {
25
+ return l.length > 0
26
+ })
27
+ .map((l) => {
28
+ return JSON.parse(l).message.id as number
29
+ })
30
+ }
31
+
32
+ describe('CDP log rotation', () => {
33
+ it('rotates when lineCount exceeds maxEntries, keeping last half', async () => {
34
+ const tmpDir = makeTmpDir()
35
+ const logFile = path.join(tmpDir, 'cdp.jsonl')
36
+ const logger = createCdpLogger({ logFilePath: logFile, maxEntries: 20 })
37
+
38
+ // Write 25 entries to trigger rotation (threshold is 20)
39
+ for (let i = 0; i < 25; i++) {
40
+ logger.log(makeEntry(i))
41
+ }
42
+ await logger.flush()
43
+
44
+ const ids = readIds(logFile)
45
+
46
+ // Rotation triggers after entry 20 is written (lineCount becomes 21 > 20).
47
+ // It keeps last 10 (entries 11-20), then entries 21-24 are appended.
48
+ expect(ids).toMatchInlineSnapshot(`
49
+ [
50
+ 11,
51
+ 12,
52
+ 13,
53
+ 14,
54
+ 15,
55
+ 16,
56
+ 17,
57
+ 18,
58
+ 19,
59
+ 20,
60
+ 21,
61
+ 22,
62
+ 23,
63
+ 24,
64
+ ]
65
+ `)
66
+
67
+ fs.rmSync(tmpDir, { recursive: true })
68
+ })
69
+
70
+ it('does not rotate when under maxEntries', async () => {
71
+ const tmpDir = makeTmpDir()
72
+ const logFile = path.join(tmpDir, 'cdp.jsonl')
73
+ const logger = createCdpLogger({ logFilePath: logFile, maxEntries: 50 })
74
+
75
+ for (let i = 0; i < 30; i++) {
76
+ logger.log(makeEntry(i))
77
+ }
78
+ await logger.flush()
79
+
80
+ const ids = readIds(logFile)
81
+ expect(ids.length).toBe(30)
82
+ expect(ids[0]).toBe(0)
83
+ expect(ids[29]).toBe(29)
84
+
85
+ fs.rmSync(tmpDir, { recursive: true })
86
+ })
87
+
88
+ it('handles multiple rotations', async () => {
89
+ const tmpDir = makeTmpDir()
90
+ const logFile = path.join(tmpDir, 'cdp.jsonl')
91
+ const logger = createCdpLogger({ logFilePath: logFile, maxEntries: 10 })
92
+
93
+ // Write 35 entries, should trigger multiple rotations
94
+ for (let i = 0; i < 35; i++) {
95
+ logger.log(makeEntry(i))
96
+ }
97
+ await logger.flush()
98
+
99
+ const ids = readIds(logFile)
100
+
101
+ // File should never exceed maxEntries
102
+ expect(ids.length).toBeLessThanOrEqual(15)
103
+ expect(ids.length).toBeGreaterThanOrEqual(5)
104
+
105
+ // Last entry should always be the most recent
106
+ expect(ids[ids.length - 1]).toBe(34)
107
+ // No entries from the very beginning should survive multiple rotations
108
+ expect(ids[0]).toBeGreaterThan(10)
109
+
110
+ fs.rmSync(tmpDir, { recursive: true })
111
+ })
112
+
113
+ it('uses atomic rename for rotation', async () => {
114
+ const tmpDir = makeTmpDir()
115
+ const logFile = path.join(tmpDir, 'cdp.jsonl')
116
+ const logger = createCdpLogger({ logFilePath: logFile, maxEntries: 10 })
117
+
118
+ for (let i = 0; i < 15; i++) {
119
+ logger.log(makeEntry(i))
120
+ }
121
+ await logger.flush()
122
+
123
+ // Temp file should not remain after successful rotation
124
+ expect(fs.existsSync(`${logFile}.tmp`)).toBe(false)
125
+
126
+ const ids = readIds(logFile)
127
+ expect(ids[ids.length - 1]).toBe(14)
128
+
129
+ fs.rmSync(tmpDir, { recursive: true })
130
+ })
131
+ })
package/src/cdp-log.ts CHANGED
@@ -12,6 +12,8 @@ export type CdpLogEntry = {
12
12
 
13
13
  export type CdpLogger = {
14
14
  log(entry: CdpLogEntry): void
15
+ /** Wait for all pending writes (and any in-flight rotation) to complete */
16
+ flush(): Promise<void>
15
17
  logFilePath: string
16
18
  }
17
19
 
@@ -41,10 +43,20 @@ function createTruncatingReplacer({ maxStringLength }: { maxStringLength: number
41
43
  }
42
44
  }
43
45
 
46
+ const DEFAULT_MAX_ENTRIES = 10_000
47
+
48
+ function resolvePositiveInt(value: number | undefined, fallback: number): number {
49
+ if (value == null || !Number.isFinite(value) || value < 2) {
50
+ return fallback
51
+ }
52
+ return Math.floor(value)
53
+ }
54
+
44
55
  export function createCdpLogger({
45
56
  logFilePath,
46
57
  maxStringLength,
47
- }: { logFilePath?: string; maxStringLength?: number } = {}): CdpLogger {
58
+ maxEntries,
59
+ }: { logFilePath?: string; maxStringLength?: number; maxEntries?: number } = {}): CdpLogger {
48
60
  const resolvedLogFilePath = logFilePath || LOG_CDP_FILE_PATH
49
61
  const logDir = path.dirname(resolvedLogFilePath)
50
62
  if (!fs.existsSync(logDir)) {
@@ -53,16 +65,46 @@ export function createCdpLogger({
53
65
  fs.writeFileSync(resolvedLogFilePath, '')
54
66
 
55
67
  let queue: Promise<void> = Promise.resolve()
68
+ let lineCount = 0
56
69
  const maxLength = maxStringLength ?? DEFAULT_MAX_STRING_LENGTH
70
+ const envMaxEntries = Number(process.env.PLAYWRITER_CDP_LOG_MAX_ENTRIES)
71
+ const resolvedMaxEntries = resolvePositiveInt(maxEntries, resolvePositiveInt(envMaxEntries, DEFAULT_MAX_ENTRIES))
72
+ // Keep half the entries after rotation so we don't rotate on every write
73
+ const keepAfterRotation = Math.floor(resolvedMaxEntries / 2)
74
+
75
+ // Atomic rotation: write to temp file then rename to avoid corruption on crash
76
+ const rotate = async (): Promise<void> => {
77
+ try {
78
+ const content = await fs.promises.readFile(resolvedLogFilePath, 'utf-8')
79
+ const lines = content.split('\n').filter((l) => {
80
+ return l.length > 0
81
+ })
82
+ const kept = lines.slice(-keepAfterRotation)
83
+ const tmpPath = `${resolvedLogFilePath}.tmp`
84
+ await fs.promises.writeFile(tmpPath, kept.join('\n') + '\n')
85
+ await fs.promises.rename(tmpPath, resolvedLogFilePath)
86
+ lineCount = kept.length
87
+ } catch {
88
+ // If rotation fails (disk error, permissions), keep logging without rotation.
89
+ // lineCount stays high so rotation will be retried on next write.
90
+ }
91
+ }
57
92
 
58
93
  const log = (entry: CdpLogEntry): void => {
59
94
  const replacer = createTruncatingReplacer({ maxStringLength: maxLength })
60
95
  const line = JSON.stringify(entry, replacer)
61
- queue = queue.then(() => fs.promises.appendFile(resolvedLogFilePath, `${line}\n`))
96
+ queue = queue.then(async () => {
97
+ await fs.promises.appendFile(resolvedLogFilePath, `${line}\n`)
98
+ lineCount++
99
+ if (lineCount > resolvedMaxEntries) {
100
+ await rotate()
101
+ }
102
+ })
62
103
  }
63
104
 
64
105
  return {
65
106
  log,
107
+ flush: () => queue,
66
108
  logFilePath: resolvedLogFilePath,
67
109
  }
68
110
  }
package/src/cdp-relay.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Hono } from 'hono'
2
2
  import { cors } from 'hono/cors'
3
- import { serve } from '@hono/node-server'
3
+ import { createAdaptorServer } from '@hono/node-server'
4
4
  import { getConnInfo } from '@hono/node-server/conninfo'
5
5
  import { createNodeWebSocket } from '@hono/node-ws'
6
6
  import type { WSContext } from 'hono/ws'
@@ -25,7 +25,7 @@ Buffer.prototype[util.inspect.custom] = function () {
25
25
  }
26
26
 
27
27
  import { EventEmitter } from 'node:events'
28
- import { VERSION, EXTENSION_IDS } from './utils.js'
28
+ import { VERSION, EXTENSION_IDS, shouldAutoEnablePlaywriter } from './utils.js'
29
29
  import { createCdpLogger, type CdpLogEntry, type CdpLogger } from './cdp-log.js'
30
30
  import { RecordingRelay } from './recording-relay.js'
31
31
  import { appendSessionToWsUrl } from './chrome-discovery.js'
@@ -521,10 +521,10 @@ export async function startPlayWriterCDPRelayServer({
521
521
  return recordingRelays.get(connId) || null
522
522
  }
523
523
 
524
- // Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist.
525
- // This allows Playwright to connect and immediately have a page to work with.
524
+ // Auto-create an initial blank tab when no targets exist. Set
525
+ // PLAYWRITER_AUTO_ENABLE=false to require manually enabled tabs instead.
526
526
  async function maybeAutoCreateInitialTab(extensionId: string): Promise<void> {
527
- if (!process.env.PLAYWRITER_AUTO_ENABLE) {
527
+ if (!shouldAutoEnablePlaywriter()) {
528
528
  return
529
529
  }
530
530
  const conn = getExtensionConnection(extensionId)
@@ -856,6 +856,87 @@ export async function startPlayWriterCDPRelayServer({
856
856
  allowMethods: ['GET', 'POST', 'HEAD', 'OPTIONS'],
857
857
  }),
858
858
  )
859
+ // Host header validation to prevent DNS rebinding attacks.
860
+ // DNS rebinding is worse than a simple cross-origin request: the attacker
861
+ // serves a page from http://evil.com:19988, then rebinds the DNS to
862
+ // 127.0.0.1. The browser now considers requests to our relay as same-origin,
863
+ // so Sec-Fetch-Site is "same-origin", CORS doesn't apply, and JSON POSTs
864
+ // don't need preflight. This bypasses all our other defenses.
865
+ // By rejecting any Host that isn't a known localhost value we kill DNS
866
+ // rebinding at the root. When a valid token is provided (remote access), we
867
+ // allow through regardless of Host since remote clients use real hostnames.
868
+ const ALLOWED_HOSTS = new Set([
869
+ 'localhost',
870
+ '127.0.0.1',
871
+ '[::1]',
872
+ '::1',
873
+ ])
874
+
875
+ // Parse the Host header into just the hostname, handling IPv6 brackets and
876
+ // port suffixes. Returns null for missing or malformed values.
877
+ function parseHostname(hostHeader: string | undefined): string | null {
878
+ const value = hostHeader?.trim().toLowerCase()
879
+ if (!value) {
880
+ return null
881
+ }
882
+ // IPv6 in brackets: [::1] or [::1]:19988
883
+ if (value.startsWith('[')) {
884
+ const closingBracket = value.indexOf(']')
885
+ if (closingBracket === -1) {
886
+ return null
887
+ }
888
+ const host = value.slice(0, closingBracket + 1)
889
+ const rest = value.slice(closingBracket + 1)
890
+ if (rest && !/^:\d+$/.test(rest)) {
891
+ return null
892
+ }
893
+ return host
894
+ }
895
+ // Bare ::1 without brackets (uncommon but possible)
896
+ if (value === '::1') {
897
+ return '::1'
898
+ }
899
+ // hostname or hostname:port
900
+ const colonIndex = value.indexOf(':')
901
+ if (colonIndex === -1) {
902
+ return value
903
+ }
904
+ const host = value.slice(0, colonIndex)
905
+ const portPart = value.slice(colonIndex + 1)
906
+ if (!/^\d+$/.test(portPart)) {
907
+ return null
908
+ }
909
+ return host || null
910
+ }
911
+
912
+ function hasValidToken(c: { req: { header: (name: string) => string | undefined; url: string } }): boolean {
913
+ if (!token) {
914
+ return false
915
+ }
916
+ const authHeader = c.req.header('authorization') || ''
917
+ const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null
918
+ const queryToken = new URL(c.req.url, 'http://localhost').searchParams.get('token')
919
+ return bearerToken === token || queryToken === token
920
+ }
921
+
922
+ app.use('*', async (c, next) => {
923
+ const hostname = parseHostname(c.req.header('host'))
924
+ if (hostname && ALLOWED_HOSTS.has(hostname)) {
925
+ return next()
926
+ }
927
+ // Remote clients with a valid token are allowed regardless of Host
928
+ if (hasValidToken(c)) {
929
+ return next()
930
+ }
931
+ // Missing Host header from non-browser clients (curl without Host) is fine
932
+ // in local mode since they're not browser-based DNS rebinding attacks
933
+ if (!hostname && !token) {
934
+ return next()
935
+ }
936
+ logger?.log(pc.red(`Rejecting request with unexpected Host header: ${c.req.header('host')} (DNS rebinding protection)`))
937
+ return c.text('Forbidden - Invalid Host header', 403)
938
+ })
939
+
859
940
  const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
860
941
 
861
942
  const getCdpWsUrl = (c: { req: { header: (name: string) => string | undefined } }) => {
@@ -2065,9 +2146,26 @@ export async function startPlayWriterCDPRelayServer({
2065
2146
  return c.json(result)
2066
2147
  })
2067
2148
 
2068
- const server = serve({ fetch: app.fetch, port, hostname: host })
2149
+ // Use createAdaptorServer instead of serve() so we control the listen()
2150
+ // timing. This lets us inject WebSocket upgrade handlers before binding and
2151
+ // await the bind to surface EADDRINUSE as a catchable error (issue #75).
2152
+ const server = createAdaptorServer({ fetch: app.fetch, hostname: host })
2069
2153
  injectWebSocket(server)
2070
2154
 
2155
+ await new Promise<void>((resolve, reject) => {
2156
+ const onListening = () => {
2157
+ server.off('error', onError)
2158
+ resolve()
2159
+ }
2160
+ const onError = (error: Error) => {
2161
+ server.off('listening', onListening)
2162
+ reject(error)
2163
+ }
2164
+ server.once('listening', onListening)
2165
+ server.once('error', onError)
2166
+ server.listen(port, host)
2167
+ })
2168
+
2071
2169
  const wsHost = `ws://${host}:${port}`
2072
2170
  const cdpEndpoint = `${wsHost}/cdp`
2073
2171
  const extensionEndpoint = `${wsHost}/extension`
package/src/cli.ts CHANGED
@@ -27,8 +27,6 @@ import { discoverChromeInstances, resolveDirectInput, type DiscoveredInstance }
27
27
 
28
28
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
29
29
 
30
- const cliRelayEnv = { PLAYWRITER_AUTO_ENABLE: '1' }
31
-
32
30
  const cli = goke('playwriter')
33
31
 
34
32
  cli
@@ -53,7 +51,7 @@ cli
53
51
  import('./package-paths.js'),
54
52
  ])
55
53
 
56
- await ensureRelayServer({ logger: console, env: cliRelayEnv })
54
+ await ensureRelayServer({ logger: console })
57
55
 
58
56
  const browserPath = resolveBrowserExecutablePath({ browserPath: binaryPath })
59
57
  const extensionPath = getBundledExtensionPath()
@@ -169,15 +167,18 @@ function buildAuthHeaders({ token, json }: { token?: string; json?: boolean }):
169
167
  return headers
170
168
  }
171
169
 
172
- async function fetchExtensionsStatus(host?: string): Promise<ExtensionStatus[]> {
170
+ async function fetchExtensionsStatus({ host, token }: { host?: string; token?: string } = {}): Promise<ExtensionStatus[]> {
173
171
  try {
174
172
  const serverUrl = await getServerUrl(host)
173
+ const headers = buildAuthHeaders({ token })
175
174
  const response = await fetch(`${serverUrl}/extensions/status`, {
176
175
  signal: AbortSignal.timeout(2000),
176
+ headers,
177
177
  })
178
178
  if (!response.ok) {
179
179
  const fallback = await fetch(`${serverUrl}/extension/status`, {
180
180
  signal: AbortSignal.timeout(2000),
181
+ headers,
181
182
  })
182
183
  if (!fallback.ok) {
183
184
  return []
@@ -234,7 +235,7 @@ async function executeCode(options: {
234
235
 
235
236
  // Ensure relay server is running (only for local)
236
237
  if (!host && !process.env.PLAYWRITER_HOST) {
237
- const restarted = await ensureRelayServer({ logger: console, env: cliRelayEnv })
238
+ const restarted = await ensureRelayServer({ logger: console })
238
239
  if (restarted) {
239
240
  const connectedExtensions = await waitForConnectedExtensions({
240
241
  logger: console,
@@ -442,7 +443,7 @@ cli
442
443
  let extensions: ExtensionStatus[] = []
443
444
 
444
445
  if (isLocal) {
445
- await ensureRelayServer({ logger: console, env: cliRelayEnv })
446
+ await ensureRelayServer({ logger: console })
446
447
  extensions = await waitForConnectedExtensions({
447
448
  timeoutMs: 12000,
448
449
  pollIntervalMs: 250,
@@ -458,7 +459,7 @@ cli
458
459
  })
459
460
  }
460
461
  } else {
461
- extensions = await fetchExtensionsStatus(options.host)
462
+ extensions = await fetchExtensionsStatus({ host: options.host, token: options.token })
462
463
  }
463
464
 
464
465
  if (extensions.length === 0) {
@@ -574,7 +575,7 @@ cli
574
575
 
575
576
  async function ensureRelayForSessionCreation(isLocal: boolean): Promise<void> {
576
577
  if (isLocal) {
577
- await ensureRelayServer({ logger: console, env: cliRelayEnv })
578
+ await ensureRelayServer({ logger: console })
578
579
  }
579
580
  }
580
581
 
@@ -658,7 +659,7 @@ cli
658
659
  .option('--token <token>', 'Authentication token (or use PLAYWRITER_TOKEN env var)')
659
660
  .action(async (options) => {
660
661
  if (!options.host && !process.env.PLAYWRITER_HOST) {
661
- await ensureRelayServer({ logger: console, env: cliRelayEnv })
662
+ await ensureRelayServer({ logger: console })
662
663
  }
663
664
 
664
665
  const serverUrl = await getServerUrl(options.host)
@@ -751,7 +752,7 @@ cli
751
752
  const serverUrl = await getServerUrl(options.host)
752
753
 
753
754
  if (!options.host && !process.env.PLAYWRITER_HOST) {
754
- await ensureRelayServer({ logger: console, env: cliRelayEnv })
755
+ await ensureRelayServer({ logger: console })
755
756
  }
756
757
 
757
758
  try {
@@ -783,7 +784,7 @@ cli
783
784
  const serverUrl = await getServerUrl(options.host)
784
785
 
785
786
  if (!options.host && !process.env.PLAYWRITER_HOST) {
786
- await ensureRelayServer({ logger: console, env: cliRelayEnv })
787
+ await ensureRelayServer({ logger: console })
787
788
  }
788
789
 
789
790
  try {
@@ -923,13 +924,13 @@ cli
923
924
 
924
925
  // Start relay if local so the extension can connect, then fetch in parallel
925
926
  if (isLocal) {
926
- await ensureRelayServer({ logger: console, env: cliRelayEnv })
927
+ await ensureRelayServer({ logger: console })
927
928
  }
928
929
 
929
930
  const [extensions, directInstances] = await Promise.all([
930
931
  isLocal
931
932
  ? waitForConnectedExtensions({ timeoutMs: 2000, pollIntervalMs: 200, logger: console })
932
- : fetchExtensionsStatus(options.host),
933
+ : fetchExtensionsStatus({ host: options.host, token: options.token }),
933
934
  isLocal ? discoverChromeInstances() : Promise.resolve([] as DiscoveredInstance[]),
934
935
  ])
935
936