@everydaydevopsio/pilot 0.5.0 → 0.6.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 (280) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/CLAUDE.md +9 -9
  3. package/README.md +108 -53
  4. package/dist/browser/browser-manager.d.ts +60 -0
  5. package/dist/browser/browser-manager.d.ts.map +1 -0
  6. package/dist/browser/browser-manager.js +348 -0
  7. package/dist/browser/browser-manager.js.map +1 -0
  8. package/dist/browser/chrome-launcher.d.ts +30 -0
  9. package/dist/browser/chrome-launcher.d.ts.map +1 -0
  10. package/dist/browser/chrome-launcher.js +217 -0
  11. package/dist/browser/chrome-launcher.js.map +1 -0
  12. package/dist/browser/connection.d.ts +23 -0
  13. package/dist/browser/connection.d.ts.map +1 -0
  14. package/dist/browser/connection.js +53 -0
  15. package/dist/browser/connection.js.map +1 -0
  16. package/dist/browser/events.d.ts +9 -0
  17. package/dist/browser/events.d.ts.map +1 -0
  18. package/dist/browser/events.js +99 -0
  19. package/dist/browser/events.js.map +1 -0
  20. package/dist/browser/index.d.ts +4 -0
  21. package/dist/browser/index.d.ts.map +1 -0
  22. package/dist/browser/index.js +4 -0
  23. package/dist/browser/index.js.map +1 -0
  24. package/dist/browser/inspect/element-ref.d.ts +23 -0
  25. package/dist/browser/inspect/element-ref.d.ts.map +1 -0
  26. package/dist/browser/inspect/element-ref.js +52 -0
  27. package/dist/browser/inspect/element-ref.js.map +1 -0
  28. package/dist/browser/inspect/snapshot.d.ts +11 -0
  29. package/dist/browser/inspect/snapshot.d.ts.map +1 -0
  30. package/dist/browser/inspect/snapshot.js +282 -0
  31. package/dist/browser/inspect/snapshot.js.map +1 -0
  32. package/dist/browser/inspect/styles.d.ts +45 -0
  33. package/dist/browser/inspect/styles.d.ts.map +1 -0
  34. package/dist/browser/inspect/styles.js +190 -0
  35. package/dist/browser/inspect/styles.js.map +1 -0
  36. package/dist/browser/inspect/types.d.ts +35 -0
  37. package/dist/browser/inspect/types.d.ts.map +1 -0
  38. package/dist/browser/inspect/types.js +2 -0
  39. package/dist/browser/inspect/types.js.map +1 -0
  40. package/dist/browser/interaction/dialogs.d.ts +18 -0
  41. package/dist/browser/interaction/dialogs.d.ts.map +1 -0
  42. package/dist/browser/interaction/dialogs.js +35 -0
  43. package/dist/browser/interaction/dialogs.js.map +1 -0
  44. package/dist/browser/interaction/downloads.d.ts +23 -0
  45. package/dist/browser/interaction/downloads.d.ts.map +1 -0
  46. package/dist/browser/interaction/downloads.js +87 -0
  47. package/dist/browser/interaction/downloads.js.map +1 -0
  48. package/dist/browser/interaction/events.d.ts +17 -0
  49. package/dist/browser/interaction/events.d.ts.map +1 -0
  50. package/dist/browser/interaction/events.js +66 -0
  51. package/dist/browser/interaction/events.js.map +1 -0
  52. package/dist/browser/interaction/ref-resolver.d.ts +18 -0
  53. package/dist/browser/interaction/ref-resolver.d.ts.map +1 -0
  54. package/dist/browser/interaction/ref-resolver.js +32 -0
  55. package/dist/browser/interaction/ref-resolver.js.map +1 -0
  56. package/dist/browser/interaction/uploads.d.ts +4 -0
  57. package/dist/browser/interaction/uploads.d.ts.map +1 -0
  58. package/dist/browser/interaction/uploads.js +43 -0
  59. package/dist/browser/interaction/uploads.js.map +1 -0
  60. package/dist/browser/network/network-buffer.d.ts +19 -0
  61. package/dist/browser/network/network-buffer.d.ts.map +1 -0
  62. package/dist/browser/network/network-buffer.js +118 -0
  63. package/dist/browser/network/network-buffer.js.map +1 -0
  64. package/dist/browser/network/network-monitor.d.ts +8 -0
  65. package/dist/browser/network/network-monitor.d.ts.map +1 -0
  66. package/dist/browser/network/network-monitor.js +85 -0
  67. package/dist/browser/network/network-monitor.js.map +1 -0
  68. package/dist/browser/network/redaction.d.ts +3 -0
  69. package/dist/browser/network/redaction.d.ts.map +1 -0
  70. package/dist/browser/network/redaction.js +36 -0
  71. package/dist/browser/network/redaction.js.map +1 -0
  72. package/dist/browser/network/types.d.ts +45 -0
  73. package/dist/browser/network/types.d.ts.map +1 -0
  74. package/dist/browser/network/types.js +2 -0
  75. package/dist/browser/network/types.js.map +1 -0
  76. package/dist/browser/performance/analyze.d.ts +5 -0
  77. package/dist/browser/performance/analyze.d.ts.map +1 -0
  78. package/dist/browser/performance/analyze.js +230 -0
  79. package/dist/browser/performance/analyze.js.map +1 -0
  80. package/dist/browser/performance/trace.d.ts +10 -0
  81. package/dist/browser/performance/trace.d.ts.map +1 -0
  82. package/dist/browser/performance/trace.js +38 -0
  83. package/dist/browser/performance/trace.js.map +1 -0
  84. package/dist/browser/performance/types.d.ts +31 -0
  85. package/dist/browser/performance/types.d.ts.map +1 -0
  86. package/dist/browser/performance/types.js +2 -0
  87. package/dist/browser/performance/types.js.map +1 -0
  88. package/dist/browser/security/origins.d.ts +18 -0
  89. package/dist/browser/security/origins.d.ts.map +1 -0
  90. package/dist/browser/security/origins.js +84 -0
  91. package/dist/browser/security/origins.js.map +1 -0
  92. package/dist/browser/tabs.d.ts +15 -0
  93. package/dist/browser/tabs.d.ts.map +1 -0
  94. package/dist/browser/tabs.js +71 -0
  95. package/dist/browser/tabs.js.map +1 -0
  96. package/dist/browser/types.d.ts +31 -0
  97. package/dist/browser/types.d.ts.map +1 -0
  98. package/dist/browser/types.js +2 -0
  99. package/dist/browser/types.js.map +1 -0
  100. package/dist/browser.d.ts +2 -99
  101. package/dist/browser.d.ts.map +1 -1
  102. package/dist/browser.js +3 -653
  103. package/dist/browser.js.map +1 -1
  104. package/dist/commands/check.d.ts +17 -0
  105. package/dist/commands/check.d.ts.map +1 -0
  106. package/dist/commands/check.js +38 -0
  107. package/dist/commands/check.js.map +1 -0
  108. package/dist/commands/click.d.ts +3 -1
  109. package/dist/commands/click.d.ts.map +1 -1
  110. package/dist/commands/click.js +16 -4
  111. package/dist/commands/click.js.map +1 -1
  112. package/dist/commands/connect.d.ts +14 -0
  113. package/dist/commands/connect.d.ts.map +1 -0
  114. package/dist/commands/connect.js +19 -0
  115. package/dist/commands/connect.js.map +1 -0
  116. package/dist/commands/dialog.d.ts +19 -0
  117. package/dist/commands/dialog.d.ts.map +1 -0
  118. package/dist/commands/dialog.js +51 -0
  119. package/dist/commands/dialog.js.map +1 -0
  120. package/dist/commands/downloads.d.ts +30 -0
  121. package/dist/commands/downloads.d.ts.map +1 -0
  122. package/dist/commands/downloads.js +66 -0
  123. package/dist/commands/downloads.js.map +1 -0
  124. package/dist/commands/fill.d.ts +16 -0
  125. package/dist/commands/fill.d.ts.map +1 -0
  126. package/dist/commands/fill.js +15 -0
  127. package/dist/commands/fill.js.map +1 -0
  128. package/dist/commands/find.d.ts +14 -0
  129. package/dist/commands/find.d.ts.map +1 -0
  130. package/dist/commands/find.js +26 -0
  131. package/dist/commands/find.js.map +1 -0
  132. package/dist/commands/hover.d.ts +13 -0
  133. package/dist/commands/hover.d.ts.map +1 -0
  134. package/dist/commands/hover.js +24 -0
  135. package/dist/commands/hover.js.map +1 -0
  136. package/dist/commands/navigate.d.ts.map +1 -1
  137. package/dist/commands/navigate.js +13 -0
  138. package/dist/commands/navigate.js.map +1 -1
  139. package/dist/commands/network.d.ts +33 -0
  140. package/dist/commands/network.d.ts.map +1 -0
  141. package/dist/commands/network.js +84 -0
  142. package/dist/commands/network.js.map +1 -0
  143. package/dist/commands/performance.d.ts +12 -0
  144. package/dist/commands/performance.d.ts.map +1 -0
  145. package/dist/commands/performance.js +26 -0
  146. package/dist/commands/performance.js.map +1 -0
  147. package/dist/commands/press_key.d.ts +10 -0
  148. package/dist/commands/press_key.d.ts.map +1 -0
  149. package/dist/commands/press_key.js +103 -0
  150. package/dist/commands/press_key.js.map +1 -0
  151. package/dist/commands/scroll.d.ts +21 -0
  152. package/dist/commands/scroll.d.ts.map +1 -0
  153. package/dist/commands/scroll.js +54 -0
  154. package/dist/commands/scroll.js.map +1 -0
  155. package/dist/commands/select.d.ts +18 -0
  156. package/dist/commands/select.d.ts.map +1 -0
  157. package/dist/commands/select.js +53 -0
  158. package/dist/commands/select.js.map +1 -0
  159. package/dist/commands/snapshot.d.ts +9 -0
  160. package/dist/commands/snapshot.d.ts.map +1 -0
  161. package/dist/commands/snapshot.js +7 -0
  162. package/dist/commands/snapshot.js.map +1 -0
  163. package/dist/commands/styles.d.ts +13 -0
  164. package/dist/commands/styles.d.ts.map +1 -0
  165. package/dist/commands/styles.js +7 -0
  166. package/dist/commands/styles.js.map +1 -0
  167. package/dist/commands/type.d.ts +3 -1
  168. package/dist/commands/type.d.ts.map +1 -1
  169. package/dist/commands/type.js +11 -2
  170. package/dist/commands/type.js.map +1 -1
  171. package/dist/commands/upload.d.ts +12 -0
  172. package/dist/commands/upload.d.ts.map +1 -0
  173. package/dist/commands/upload.js +13 -0
  174. package/dist/commands/upload.js.map +1 -0
  175. package/dist/mcp/console-buffer.d.ts +9 -0
  176. package/dist/mcp/console-buffer.d.ts.map +1 -1
  177. package/dist/mcp/console-buffer.js.map +1 -1
  178. package/dist/mcp/server.d.ts +10 -0
  179. package/dist/mcp/server.d.ts.map +1 -1
  180. package/dist/mcp/server.js +63 -2
  181. package/dist/mcp/server.js.map +1 -1
  182. package/dist/mcp/tools/browser.d.ts.map +1 -1
  183. package/dist/mcp/tools/browser.js +99 -8
  184. package/dist/mcp/tools/browser.js.map +1 -1
  185. package/dist/mcp/tools/connect.d.ts +5 -0
  186. package/dist/mcp/tools/connect.d.ts.map +1 -0
  187. package/dist/mcp/tools/connect.js +48 -0
  188. package/dist/mcp/tools/connect.js.map +1 -0
  189. package/dist/mcp/tools/files.d.ts +4 -0
  190. package/dist/mcp/tools/files.d.ts.map +1 -0
  191. package/dist/mcp/tools/files.js +85 -0
  192. package/dist/mcp/tools/files.js.map +1 -0
  193. package/dist/mcp/tools/interaction.d.ts +4 -0
  194. package/dist/mcp/tools/interaction.d.ts.map +1 -0
  195. package/dist/mcp/tools/interaction.js +176 -0
  196. package/dist/mcp/tools/interaction.js.map +1 -0
  197. package/dist/mcp/tools/names.d.ts +1 -1
  198. package/dist/mcp/tools/names.d.ts.map +1 -1
  199. package/dist/mcp/tools/names.js +19 -1
  200. package/dist/mcp/tools/names.js.map +1 -1
  201. package/dist/mcp/tools/network.d.ts +4 -0
  202. package/dist/mcp/tools/network.d.ts.map +1 -0
  203. package/dist/mcp/tools/network.js +68 -0
  204. package/dist/mcp/tools/network.js.map +1 -0
  205. package/dist/mcp/tools/performance.d.ts +4 -0
  206. package/dist/mcp/tools/performance.d.ts.map +1 -0
  207. package/dist/mcp/tools/performance.js +26 -0
  208. package/dist/mcp/tools/performance.js.map +1 -0
  209. package/dist/mcp/tools/runtime.d.ts +4 -0
  210. package/dist/mcp/tools/runtime.d.ts.map +1 -0
  211. package/dist/mcp/tools/runtime.js +128 -0
  212. package/dist/mcp/tools/runtime.js.map +1 -0
  213. package/dist/mcp/tools/snapshot.d.ts +4 -0
  214. package/dist/mcp/tools/snapshot.d.ts.map +1 -0
  215. package/dist/mcp/tools/snapshot.js +42 -0
  216. package/dist/mcp/tools/snapshot.js.map +1 -0
  217. package/dist/mcp/tools/styles.d.ts +4 -0
  218. package/dist/mcp/tools/styles.d.ts.map +1 -0
  219. package/dist/mcp/tools/styles.js +27 -0
  220. package/dist/mcp/tools/styles.js.map +1 -0
  221. package/dist/util/config.d.ts.map +1 -1
  222. package/dist/util/config.js +3 -2
  223. package/dist/util/config.js.map +1 -1
  224. package/package.json +7 -3
  225. package/src/browser/browser-manager.ts +457 -0
  226. package/src/browser/chrome-launcher.ts +271 -0
  227. package/src/browser/connection.ts +84 -0
  228. package/src/browser/events.ts +121 -0
  229. package/src/browser/index.ts +20 -0
  230. package/src/browser/inspect/element-ref.ts +68 -0
  231. package/src/browser/inspect/snapshot.ts +348 -0
  232. package/src/browser/inspect/styles.ts +258 -0
  233. package/src/browser/inspect/types.ts +37 -0
  234. package/src/browser/interaction/dialogs.ts +54 -0
  235. package/src/browser/interaction/downloads.ts +116 -0
  236. package/src/browser/interaction/events.ts +79 -0
  237. package/src/browser/interaction/ref-resolver.ts +53 -0
  238. package/src/browser/interaction/uploads.ts +60 -0
  239. package/src/browser/network/network-buffer.ts +133 -0
  240. package/src/browser/network/network-monitor.ts +107 -0
  241. package/src/browser/network/redaction.ts +42 -0
  242. package/src/browser/network/types.ts +47 -0
  243. package/src/browser/performance/analyze.ts +298 -0
  244. package/src/browser/performance/trace.ts +60 -0
  245. package/src/browser/performance/types.ts +34 -0
  246. package/src/browser/security/origins.ts +102 -0
  247. package/src/browser/tabs.ts +110 -0
  248. package/src/browser/types.ts +31 -0
  249. package/src/browser.ts +22 -798
  250. package/src/commands/check.ts +59 -0
  251. package/src/commands/click.ts +18 -4
  252. package/src/commands/connect.ts +40 -0
  253. package/src/commands/dialog.ts +79 -0
  254. package/src/commands/downloads.ts +111 -0
  255. package/src/commands/fill.ts +34 -0
  256. package/src/commands/find.ts +52 -0
  257. package/src/commands/hover.ts +42 -0
  258. package/src/commands/navigate.ts +20 -0
  259. package/src/commands/network.ts +141 -0
  260. package/src/commands/performance.ts +51 -0
  261. package/src/commands/press_key.ts +134 -0
  262. package/src/commands/scroll.ts +79 -0
  263. package/src/commands/select.ts +78 -0
  264. package/src/commands/snapshot.ts +18 -0
  265. package/src/commands/styles.ts +24 -0
  266. package/src/commands/type.ts +12 -2
  267. package/src/commands/upload.ts +36 -0
  268. package/src/mcp/console-buffer.ts +10 -0
  269. package/src/mcp/server.ts +74 -2
  270. package/src/mcp/tools/browser.ts +113 -7
  271. package/src/mcp/tools/connect.ts +62 -0
  272. package/src/mcp/tools/files.ts +120 -0
  273. package/src/mcp/tools/interaction.ts +232 -0
  274. package/src/mcp/tools/names.ts +19 -1
  275. package/src/mcp/tools/network.ts +86 -0
  276. package/src/mcp/tools/performance.ts +40 -0
  277. package/src/mcp/tools/runtime.ts +164 -0
  278. package/src/mcp/tools/snapshot.ts +64 -0
  279. package/src/mcp/tools/styles.ts +41 -0
  280. package/src/util/config.ts +3 -2
@@ -0,0 +1,116 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+ import { tmpdir } from 'os';
3
+ import { resolve } from 'path';
4
+
5
+ export interface DownloadRecord {
6
+ guid: string;
7
+ url: string;
8
+ suggestedFilename: string;
9
+ state: 'inProgress' | 'completed' | 'canceled';
10
+ receivedBytes: number;
11
+ totalBytes: number;
12
+ timestamp: number;
13
+ }
14
+
15
+ export class DownloadTracker {
16
+ private downloads = new Map<string, DownloadRecord>();
17
+
18
+ track(record: DownloadRecord): void {
19
+ this.downloads.set(record.guid, record);
20
+ }
21
+
22
+ update(guid: string, patch: Partial<DownloadRecord>): void {
23
+ const existing = this.downloads.get(guid);
24
+ if (existing) {
25
+ Object.assign(existing, patch);
26
+ }
27
+ }
28
+
29
+ get(guid: string): DownloadRecord | undefined {
30
+ return this.downloads.get(guid);
31
+ }
32
+
33
+ list(): DownloadRecord[] {
34
+ return [...this.downloads.values()];
35
+ }
36
+
37
+ clear(): number {
38
+ const count = this.downloads.size;
39
+ this.downloads.clear();
40
+ return count;
41
+ }
42
+
43
+ size(): number {
44
+ return this.downloads.size;
45
+ }
46
+ }
47
+
48
+ function getDownloadDir(): string {
49
+ const env = process.env.PILOT_DOWNLOAD_DIR;
50
+ if (env) return resolve(env);
51
+ return resolve(tmpdir(), 'pilot-downloads');
52
+ }
53
+
54
+ export async function enableDownloads(client: Client): Promise<string> {
55
+ const downloadPath = getDownloadDir();
56
+ await client.Browser.setDownloadBehavior({
57
+ behavior: 'allowAndName',
58
+ downloadPath,
59
+ eventsEnabled: true
60
+ });
61
+ return downloadPath;
62
+ }
63
+
64
+ export function attachDownloadHandler(
65
+ client: Client,
66
+ tracker: DownloadTracker
67
+ ): void {
68
+ client.Browser.downloadWillBegin((params) => {
69
+ tracker.track({
70
+ guid: params.guid,
71
+ url: params.url,
72
+ suggestedFilename: params.suggestedFilename,
73
+ state: 'inProgress',
74
+ receivedBytes: 0,
75
+ totalBytes: 0,
76
+ timestamp: Date.now()
77
+ });
78
+ });
79
+
80
+ client.Browser.downloadProgress((params) => {
81
+ const state =
82
+ params.state === 'completed'
83
+ ? 'completed'
84
+ : params.state === 'canceled'
85
+ ? 'canceled'
86
+ : 'inProgress';
87
+ tracker.update(params.guid, {
88
+ state,
89
+ receivedBytes: params.receivedBytes ?? 0,
90
+ totalBytes: params.totalBytes ?? 0
91
+ });
92
+ });
93
+ }
94
+
95
+ export function waitForDownload(
96
+ tracker: DownloadTracker,
97
+ guid: string,
98
+ timeoutMs = 30000
99
+ ): Promise<DownloadRecord> {
100
+ return new Promise((resolve, reject) => {
101
+ const start = Date.now();
102
+ const check = () => {
103
+ const record = tracker.get(guid);
104
+ if (record && record.state !== 'inProgress') {
105
+ resolve(record);
106
+ return;
107
+ }
108
+ if (Date.now() - start > timeoutMs) {
109
+ reject(new Error(`Download ${guid} timed out after ${timeoutMs}ms`));
110
+ return;
111
+ }
112
+ setTimeout(check, 200);
113
+ };
114
+ check();
115
+ });
116
+ }
@@ -0,0 +1,79 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+
3
+ /**
4
+ * Fire framework-compatible input events on an element.
5
+ * Uses the native value setter to work with React's synthetic event system,
6
+ * then dispatches native DOM events with bubbling enabled for Vue/Svelte.
7
+ */
8
+ export async function fireInputEvents(
9
+ client: Client,
10
+ objectId: string
11
+ ): Promise<void> {
12
+ await client.Runtime.callFunctionOn({
13
+ objectId,
14
+ functionDeclaration: `function() {
15
+ this.dispatchEvent(new Event('input', { bubbles: true }));
16
+ this.dispatchEvent(new Event('change', { bubbles: true }));
17
+ }`,
18
+ awaitPromise: false
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Set a form field value using the native setter for React compatibility,
24
+ * then fire input/change events.
25
+ */
26
+ export async function setFieldValue(
27
+ client: Client,
28
+ objectId: string,
29
+ value: string
30
+ ): Promise<void> {
31
+ await client.Runtime.callFunctionOn({
32
+ objectId,
33
+ functionDeclaration: `function(newValue) {
34
+ var tag = this.tagName.toLowerCase();
35
+ if (tag === 'select') {
36
+ // Select element — set by value
37
+ for (var i = 0; i < this.options.length; i++) {
38
+ if (this.options[i].value === newValue) {
39
+ this.selectedIndex = i;
40
+ break;
41
+ }
42
+ }
43
+ } else if (this.getAttribute('contenteditable') !== null) {
44
+ this.textContent = newValue;
45
+ } else {
46
+ // Use native setter for React compatibility
47
+ var proto = tag === 'textarea'
48
+ ? HTMLTextAreaElement.prototype
49
+ : HTMLInputElement.prototype;
50
+ var setter = Object.getOwnPropertyDescriptor(proto, 'value');
51
+ if (setter && setter.set) {
52
+ setter.set.call(this, newValue);
53
+ } else {
54
+ this.value = newValue;
55
+ }
56
+ }
57
+ this.dispatchEvent(new Event('input', { bubbles: true }));
58
+ this.dispatchEvent(new Event('change', { bubbles: true }));
59
+ }`,
60
+ arguments: [{ value }],
61
+ awaitPromise: false
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Get the tag name of an element by its remote object ID.
67
+ */
68
+ export async function getTagName(
69
+ client: Client,
70
+ objectId: string
71
+ ): Promise<string> {
72
+ const { result } = await client.Runtime.callFunctionOn({
73
+ objectId,
74
+ functionDeclaration: `function() { return this.tagName ? this.tagName.toLowerCase() : ''; }`,
75
+ returnByValue: true,
76
+ awaitPromise: false
77
+ });
78
+ return String(result.value ?? '');
79
+ }
@@ -0,0 +1,53 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+ import type { ElementRefMap } from '../inspect/element-ref.js';
3
+
4
+ export interface ResolvedElement {
5
+ backendNodeId: number;
6
+ objectId: string;
7
+ x: number;
8
+ y: number;
9
+ }
10
+
11
+ /**
12
+ * Resolve an element ref to coordinates and a remote object ID.
13
+ * Scrolls the element into view first so click coordinates are valid.
14
+ */
15
+ export async function resolveRef(
16
+ client: Client,
17
+ refMap: ElementRefMap,
18
+ ref: string
19
+ ): Promise<ResolvedElement> {
20
+ const backendNodeId = refMap.resolve(ref);
21
+
22
+ // Scroll into view
23
+ await client.DOM.scrollIntoViewIfNeeded({ backendNodeId });
24
+
25
+ // Get center coordinates from box model
26
+ const { model } = await client.DOM.getBoxModel({ backendNodeId });
27
+ const content = model.content;
28
+ const x = (content[0] + content[2] + content[4] + content[6]) / 4;
29
+ const y = (content[1] + content[3] + content[5] + content[7]) / 4;
30
+
31
+ // Get remote object for Runtime.callFunctionOn
32
+ const { object } = await client.DOM.resolveNode({ backendNodeId });
33
+ if (!object.objectId) {
34
+ throw new Error(`Could not resolve ref "${ref}" to a remote object`);
35
+ }
36
+
37
+ return {
38
+ backendNodeId,
39
+ objectId: object.objectId,
40
+ x: Math.round(x),
41
+ y: Math.round(y)
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Focus an element by ref.
47
+ */
48
+ export async function focusRef(
49
+ client: Client,
50
+ backendNodeId: number
51
+ ): Promise<void> {
52
+ await client.DOM.focus({ backendNodeId });
53
+ }
@@ -0,0 +1,60 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+ import { existsSync, statSync, realpathSync } from 'fs';
3
+ import { resolve, normalize } from 'path';
4
+
5
+ const DEFAULT_UPLOAD_ROOTS = [process.cwd()];
6
+
7
+ function getUploadRoots(): string[] {
8
+ const env = process.env.PILOT_UPLOAD_ROOTS;
9
+ if (env) {
10
+ return env
11
+ .split(',')
12
+ .map((p) => p.trim())
13
+ .filter((p) => p.length > 0)
14
+ .map((p) => resolve(p));
15
+ }
16
+ return DEFAULT_UPLOAD_ROOTS;
17
+ }
18
+
19
+ function isUnderAllowedRoot(filePath: string, roots: string[]): boolean {
20
+ const resolved = realpathSync(filePath);
21
+ // Use path separator boundary to prevent "/allowed" matching "/allowed_evil"
22
+ return roots.some(
23
+ (root) => resolved === root || resolved.startsWith(root + '/')
24
+ );
25
+ }
26
+
27
+ export function validateUploadFiles(files: string[]): void {
28
+ const roots = getUploadRoots();
29
+
30
+ for (const file of files) {
31
+ const normalized = normalize(resolve(file));
32
+
33
+ if (!existsSync(normalized)) {
34
+ throw new Error(`Upload file not found: ${file}`);
35
+ }
36
+
37
+ const stat = statSync(normalized);
38
+ if (!stat.isFile()) {
39
+ throw new Error(`Upload path is not a regular file: ${file}`);
40
+ }
41
+
42
+ if (!isUnderAllowedRoot(normalized, roots)) {
43
+ throw new Error(
44
+ `Upload file "${file}" is outside allowed roots. Set PILOT_UPLOAD_ROOTS to configure allowed directories.`
45
+ );
46
+ }
47
+ }
48
+ }
49
+
50
+ export async function uploadFiles(
51
+ client: Client,
52
+ backendNodeId: number,
53
+ files: string[]
54
+ ): Promise<void> {
55
+ const resolved = files.map((f) => resolve(f));
56
+ await client.DOM.setFileInputFiles({
57
+ files: resolved,
58
+ backendNodeId
59
+ });
60
+ }
@@ -0,0 +1,133 @@
1
+ import type { NetworkRecord, NetworkSummary, NetworkFilter } from './types.js';
2
+ import { redactHeaders } from './redaction.js';
3
+
4
+ export class NetworkBuffer {
5
+ private buffer: NetworkRecord[] = [];
6
+ private head = 0;
7
+ private count = 0;
8
+ private index = new Map<string, number>();
9
+
10
+ constructor(private readonly maxSize: number = 1000) {}
11
+
12
+ push(record: NetworkRecord): void {
13
+ if (this.count < this.maxSize) {
14
+ const idx = this.buffer.length;
15
+ this.buffer.push(record);
16
+ this.index.set(record.id, idx);
17
+ this.count++;
18
+ } else {
19
+ // Remove the old record's index entry
20
+ const oldRecord = this.buffer[this.head];
21
+ if (oldRecord) {
22
+ this.index.delete(oldRecord.id);
23
+ }
24
+ this.buffer[this.head] = record;
25
+ this.index.set(record.id, this.head);
26
+ this.head = (this.head + 1) % this.maxSize;
27
+ }
28
+ }
29
+
30
+ update(id: string, patch: Partial<NetworkRecord>): void {
31
+ const idx = this.index.get(id);
32
+ if (idx === undefined) return;
33
+ Object.assign(this.buffer[idx], patch);
34
+ }
35
+
36
+ get(id: string): NetworkRecord | undefined {
37
+ const idx = this.index.get(id);
38
+ if (idx === undefined) return undefined;
39
+ return this.buffer[idx];
40
+ }
41
+
42
+ list(filter?: NetworkFilter): NetworkSummary[] {
43
+ const records = this.toArray();
44
+ const filtered = this.applyFilter(records, filter);
45
+ return filtered.map((r) => ({
46
+ id: r.id,
47
+ method: r.method,
48
+ url: r.url,
49
+ status: r.status,
50
+ resourceType: r.resourceType,
51
+ duration: r.duration,
52
+ mimeType: r.mimeType,
53
+ size: r.encodedDataLength,
54
+ failed: r.failed,
55
+ fromCache: r.fromCache
56
+ }));
57
+ }
58
+
59
+ getDetail(id: string): NetworkRecord | undefined {
60
+ const record = this.get(id);
61
+ if (!record) return undefined;
62
+ return {
63
+ ...record,
64
+ requestHeaders: redactHeaders(record.requestHeaders),
65
+ responseHeaders: redactHeaders(record.responseHeaders)
66
+ };
67
+ }
68
+
69
+ clear(): number {
70
+ const cleared = this.count;
71
+ this.buffer = [];
72
+ this.head = 0;
73
+ this.count = 0;
74
+ this.index.clear();
75
+ return cleared;
76
+ }
77
+
78
+ size(): number {
79
+ return this.count;
80
+ }
81
+
82
+ private toArray(): NetworkRecord[] {
83
+ if (this.count < this.maxSize) {
84
+ return [...this.buffer];
85
+ }
86
+ return [
87
+ ...this.buffer.slice(this.head),
88
+ ...this.buffer.slice(0, this.head)
89
+ ];
90
+ }
91
+
92
+ private applyFilter(
93
+ records: NetworkRecord[],
94
+ filter?: NetworkFilter
95
+ ): NetworkRecord[] {
96
+ if (!filter) return records;
97
+
98
+ let result = records;
99
+
100
+ if (filter.url) {
101
+ const urlLower = filter.url.toLowerCase();
102
+ result = result.filter((r) => r.url.toLowerCase().includes(urlLower));
103
+ }
104
+
105
+ if (filter.method) {
106
+ const methodUpper = filter.method.toUpperCase();
107
+ result = result.filter((r) => r.method === methodUpper);
108
+ }
109
+
110
+ if (filter.statusMin !== undefined) {
111
+ result = result.filter((r) => r.status >= filter.statusMin!);
112
+ }
113
+
114
+ if (filter.statusMax !== undefined) {
115
+ result = result.filter((r) => r.status <= filter.statusMax!);
116
+ }
117
+
118
+ if (filter.resourceType) {
119
+ const rt = filter.resourceType.toLowerCase();
120
+ result = result.filter((r) => r.resourceType.toLowerCase() === rt);
121
+ }
122
+
123
+ if (filter.failed !== undefined) {
124
+ result = result.filter((r) => r.failed === filter.failed);
125
+ }
126
+
127
+ if (filter.limit !== undefined && result.length > filter.limit) {
128
+ result = result.slice(-filter.limit);
129
+ }
130
+
131
+ return result;
132
+ }
133
+ }
@@ -0,0 +1,107 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+ import type { NetworkRecord } from './types.js';
3
+ import type { NetworkBuffer } from './network-buffer.js';
4
+
5
+ /**
6
+ * Parse CDP headers object to a flat Record.
7
+ * CDP returns headers as { "Header-Name": "value" }.
8
+ */
9
+ function parseHeaders(
10
+ headers?: Record<string, string>
11
+ ): Record<string, string> {
12
+ if (!headers) return {};
13
+ return { ...headers };
14
+ }
15
+
16
+ export function attachNetworkMonitor(
17
+ client: Client,
18
+ buffer: NetworkBuffer
19
+ ): void {
20
+ client.Network.requestWillBeSent((params) => {
21
+ const record: NetworkRecord = {
22
+ id: params.requestId,
23
+ method: params.request.method,
24
+ url: params.request.url,
25
+ status: 0,
26
+ resourceType: params.type ?? 'Other',
27
+ startTime: Date.now(),
28
+ failed: false,
29
+ requestHeaders: parseHeaders(
30
+ params.request.headers as Record<string, string>
31
+ ),
32
+ responseHeaders: {},
33
+ mimeType: '',
34
+ encodedDataLength: 0,
35
+ fromCache: false,
36
+ complete: false
37
+ };
38
+ buffer.push(record);
39
+ });
40
+
41
+ client.Network.responseReceived((params) => {
42
+ buffer.update(params.requestId, {
43
+ status: params.response.status,
44
+ responseHeaders: parseHeaders(
45
+ params.response.headers as Record<string, string>
46
+ ),
47
+ mimeType: params.response.mimeType ?? '',
48
+ fromCache: params.response.fromDiskCache ?? false
49
+ });
50
+ });
51
+
52
+ client.Network.loadingFinished((params) => {
53
+ const now = Date.now();
54
+ const existing = buffer.get(params.requestId);
55
+ buffer.update(params.requestId, {
56
+ endTime: now,
57
+ duration: existing?.startTime ? now - existing.startTime : undefined,
58
+ encodedDataLength: params.encodedDataLength ?? 0,
59
+ complete: true
60
+ });
61
+ });
62
+
63
+ client.Network.loadingFailed((params) => {
64
+ const now = Date.now();
65
+ const existing = buffer.get(params.requestId);
66
+ buffer.update(params.requestId, {
67
+ endTime: now,
68
+ duration: existing?.startTime ? now - existing.startTime : undefined,
69
+ failed: true,
70
+ errorText: params.errorText,
71
+ complete: true
72
+ });
73
+ });
74
+ }
75
+
76
+ const MAX_RESPONSE_BODY_SIZE = 1024 * 1024; // 1 MB
77
+
78
+ export async function getResponseBody(
79
+ client: Client,
80
+ requestId: string
81
+ ): Promise<{ body: string; truncated: boolean } | null> {
82
+ try {
83
+ const { body, base64Encoded } = await client.Network.getResponseBody({
84
+ requestId
85
+ });
86
+
87
+ if (base64Encoded) {
88
+ const decoded = Buffer.from(body, 'base64');
89
+ if (decoded.length > MAX_RESPONSE_BODY_SIZE) {
90
+ return {
91
+ body: decoded.subarray(0, MAX_RESPONSE_BODY_SIZE).toString('utf-8'),
92
+ truncated: true
93
+ };
94
+ }
95
+ return { body: decoded.toString('utf-8'), truncated: false };
96
+ }
97
+
98
+ if (body.length > MAX_RESPONSE_BODY_SIZE) {
99
+ return { body: body.slice(0, MAX_RESPONSE_BODY_SIZE), truncated: true };
100
+ }
101
+
102
+ return { body, truncated: false };
103
+ } catch {
104
+ // Body may not be available (e.g., still loading, or was a redirect)
105
+ return null;
106
+ }
107
+ }
@@ -0,0 +1,42 @@
1
+ const DEFAULT_REDACT_HEADERS = new Set([
2
+ 'authorization',
3
+ 'proxy-authorization',
4
+ 'cookie',
5
+ 'set-cookie',
6
+ 'x-api-key'
7
+ ]);
8
+
9
+ let customRedactHeaders: Set<string> | null = null;
10
+
11
+ export function getRedactHeaders(): Set<string> {
12
+ if (customRedactHeaders) return customRedactHeaders;
13
+
14
+ const env = process.env.PILOT_REDACT_HEADERS;
15
+ if (env) {
16
+ customRedactHeaders = new Set(
17
+ env.split(',').map((h) => h.trim().toLowerCase())
18
+ );
19
+ // Merge with defaults
20
+ for (const h of DEFAULT_REDACT_HEADERS) {
21
+ customRedactHeaders.add(h);
22
+ }
23
+ return customRedactHeaders;
24
+ }
25
+
26
+ return DEFAULT_REDACT_HEADERS;
27
+ }
28
+
29
+ export function redactHeaders(
30
+ headers: Record<string, string>
31
+ ): Record<string, string> {
32
+ const redactSet = getRedactHeaders();
33
+ const result: Record<string, string> = {};
34
+ for (const [key, value] of Object.entries(headers)) {
35
+ if (redactSet.has(key.toLowerCase())) {
36
+ result[key] = '[REDACTED]';
37
+ } else {
38
+ result[key] = value;
39
+ }
40
+ }
41
+ return result;
42
+ }
@@ -0,0 +1,47 @@
1
+ export interface NetworkRecord {
2
+ id: string;
3
+ method: string;
4
+ url: string;
5
+ status: number;
6
+ resourceType: string;
7
+ startTime: number;
8
+ endTime?: number;
9
+ duration?: number;
10
+ failed: boolean;
11
+ errorText?: string;
12
+ requestHeaders: Record<string, string>;
13
+ responseHeaders: Record<string, string>;
14
+ mimeType: string;
15
+ encodedDataLength: number;
16
+ fromCache: boolean;
17
+ complete: boolean;
18
+ }
19
+
20
+ export interface NetworkSummary {
21
+ id: string;
22
+ method: string;
23
+ url: string;
24
+ status: number;
25
+ resourceType: string;
26
+ duration?: number;
27
+ mimeType: string;
28
+ size: number;
29
+ failed: boolean;
30
+ fromCache: boolean;
31
+ }
32
+
33
+ export interface NetworkDetail extends NetworkRecord {
34
+ requestBody?: string;
35
+ responseBody?: string;
36
+ bodyTruncated?: boolean;
37
+ }
38
+
39
+ export interface NetworkFilter {
40
+ url?: string;
41
+ method?: string;
42
+ statusMin?: number;
43
+ statusMax?: number;
44
+ resourceType?: string;
45
+ failed?: boolean;
46
+ limit?: number;
47
+ }