@everydaydevopsio/pilot 0.5.2 → 0.6.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.
- package/.mcp.json +9 -0
- package/AGENTS.md +9 -0
- package/CHANGELOG.md +64 -0
- package/CLAUDE.md +7 -7
- package/README.md +282 -69
- package/dist/browser/browser-manager.d.ts +60 -0
- package/dist/browser/browser-manager.d.ts.map +1 -0
- package/dist/browser/browser-manager.js +355 -0
- package/dist/browser/browser-manager.js.map +1 -0
- package/dist/browser/chrome-launcher.d.ts +31 -0
- package/dist/browser/chrome-launcher.d.ts.map +1 -0
- package/dist/browser/chrome-launcher.js +225 -0
- package/dist/browser/chrome-launcher.js.map +1 -0
- package/dist/browser/connection.d.ts +23 -0
- package/dist/browser/connection.d.ts.map +1 -0
- package/dist/browser/connection.js +53 -0
- package/dist/browser/connection.js.map +1 -0
- package/dist/browser/events.d.ts +9 -0
- package/dist/browser/events.d.ts.map +1 -0
- package/dist/browser/events.js +99 -0
- package/dist/browser/events.js.map +1 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/inspect/element-ref.d.ts +23 -0
- package/dist/browser/inspect/element-ref.d.ts.map +1 -0
- package/dist/browser/inspect/element-ref.js +52 -0
- package/dist/browser/inspect/element-ref.js.map +1 -0
- package/dist/browser/inspect/snapshot.d.ts +11 -0
- package/dist/browser/inspect/snapshot.d.ts.map +1 -0
- package/dist/browser/inspect/snapshot.js +282 -0
- package/dist/browser/inspect/snapshot.js.map +1 -0
- package/dist/browser/inspect/styles.d.ts +45 -0
- package/dist/browser/inspect/styles.d.ts.map +1 -0
- package/dist/browser/inspect/styles.js +190 -0
- package/dist/browser/inspect/styles.js.map +1 -0
- package/dist/browser/inspect/types.d.ts +35 -0
- package/dist/browser/inspect/types.d.ts.map +1 -0
- package/dist/browser/inspect/types.js +2 -0
- package/dist/browser/inspect/types.js.map +1 -0
- package/dist/browser/interaction/dialogs.d.ts +18 -0
- package/dist/browser/interaction/dialogs.d.ts.map +1 -0
- package/dist/browser/interaction/dialogs.js +35 -0
- package/dist/browser/interaction/dialogs.js.map +1 -0
- package/dist/browser/interaction/downloads.d.ts +23 -0
- package/dist/browser/interaction/downloads.d.ts.map +1 -0
- package/dist/browser/interaction/downloads.js +87 -0
- package/dist/browser/interaction/downloads.js.map +1 -0
- package/dist/browser/interaction/events.d.ts +17 -0
- package/dist/browser/interaction/events.d.ts.map +1 -0
- package/dist/browser/interaction/events.js +66 -0
- package/dist/browser/interaction/events.js.map +1 -0
- package/dist/browser/interaction/ref-resolver.d.ts +18 -0
- package/dist/browser/interaction/ref-resolver.d.ts.map +1 -0
- package/dist/browser/interaction/ref-resolver.js +32 -0
- package/dist/browser/interaction/ref-resolver.js.map +1 -0
- package/dist/browser/interaction/uploads.d.ts +4 -0
- package/dist/browser/interaction/uploads.d.ts.map +1 -0
- package/dist/browser/interaction/uploads.js +43 -0
- package/dist/browser/interaction/uploads.js.map +1 -0
- package/dist/browser/network/network-buffer.d.ts +19 -0
- package/dist/browser/network/network-buffer.d.ts.map +1 -0
- package/dist/browser/network/network-buffer.js +118 -0
- package/dist/browser/network/network-buffer.js.map +1 -0
- package/dist/browser/network/network-monitor.d.ts +8 -0
- package/dist/browser/network/network-monitor.d.ts.map +1 -0
- package/dist/browser/network/network-monitor.js +85 -0
- package/dist/browser/network/network-monitor.js.map +1 -0
- package/dist/browser/network/redaction.d.ts +3 -0
- package/dist/browser/network/redaction.d.ts.map +1 -0
- package/dist/browser/network/redaction.js +36 -0
- package/dist/browser/network/redaction.js.map +1 -0
- package/dist/browser/network/types.d.ts +45 -0
- package/dist/browser/network/types.d.ts.map +1 -0
- package/dist/browser/network/types.js +2 -0
- package/dist/browser/network/types.js.map +1 -0
- package/dist/browser/performance/analyze.d.ts +5 -0
- package/dist/browser/performance/analyze.d.ts.map +1 -0
- package/dist/browser/performance/analyze.js +230 -0
- package/dist/browser/performance/analyze.js.map +1 -0
- package/dist/browser/performance/trace.d.ts +10 -0
- package/dist/browser/performance/trace.d.ts.map +1 -0
- package/dist/browser/performance/trace.js +38 -0
- package/dist/browser/performance/trace.js.map +1 -0
- package/dist/browser/performance/types.d.ts +31 -0
- package/dist/browser/performance/types.d.ts.map +1 -0
- package/dist/browser/performance/types.js +2 -0
- package/dist/browser/performance/types.js.map +1 -0
- package/dist/browser/preflight.d.ts +20 -0
- package/dist/browser/preflight.d.ts.map +1 -0
- package/dist/browser/preflight.js +105 -0
- package/dist/browser/preflight.js.map +1 -0
- package/dist/browser/security/origins.d.ts +18 -0
- package/dist/browser/security/origins.d.ts.map +1 -0
- package/dist/browser/security/origins.js +84 -0
- package/dist/browser/security/origins.js.map +1 -0
- package/dist/browser/tabs.d.ts +15 -0
- package/dist/browser/tabs.d.ts.map +1 -0
- package/dist/browser/tabs.js +71 -0
- package/dist/browser/tabs.js.map +1 -0
- package/dist/browser/types.d.ts +32 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/browser.d.ts +2 -99
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +3 -653
- package/dist/browser.js.map +1 -1
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +33 -48
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/linux-check.d.ts +2 -0
- package/dist/cli/linux-check.d.ts.map +1 -0
- package/dist/cli/linux-check.js +20 -0
- package/dist/cli/linux-check.js.map +1 -0
- package/dist/cli/skill-template.d.ts +1 -1
- package/dist/cli/skill-template.d.ts.map +1 -1
- package/dist/cli/skill-template.js +3 -113
- package/dist/cli/skill-template.js.map +1 -1
- package/dist/commands/check.d.ts +17 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +38 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/click.d.ts +3 -1
- package/dist/commands/click.d.ts.map +1 -1
- package/dist/commands/click.js +16 -4
- package/dist/commands/click.js.map +1 -1
- package/dist/commands/connect.d.ts +14 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +19 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/dialog.d.ts +19 -0
- package/dist/commands/dialog.d.ts.map +1 -0
- package/dist/commands/dialog.js +51 -0
- package/dist/commands/dialog.js.map +1 -0
- package/dist/commands/downloads.d.ts +30 -0
- package/dist/commands/downloads.d.ts.map +1 -0
- package/dist/commands/downloads.js +66 -0
- package/dist/commands/downloads.js.map +1 -0
- package/dist/commands/fill.d.ts +16 -0
- package/dist/commands/fill.d.ts.map +1 -0
- package/dist/commands/fill.js +15 -0
- package/dist/commands/fill.js.map +1 -0
- package/dist/commands/find.d.ts +14 -0
- package/dist/commands/find.d.ts.map +1 -0
- package/dist/commands/find.js +26 -0
- package/dist/commands/find.js.map +1 -0
- package/dist/commands/hover.d.ts +13 -0
- package/dist/commands/hover.d.ts.map +1 -0
- package/dist/commands/hover.js +24 -0
- package/dist/commands/hover.js.map +1 -0
- package/dist/commands/navigate.d.ts.map +1 -1
- package/dist/commands/navigate.js +13 -0
- package/dist/commands/navigate.js.map +1 -1
- package/dist/commands/network.d.ts +33 -0
- package/dist/commands/network.d.ts.map +1 -0
- package/dist/commands/network.js +84 -0
- package/dist/commands/network.js.map +1 -0
- package/dist/commands/performance.d.ts +12 -0
- package/dist/commands/performance.d.ts.map +1 -0
- package/dist/commands/performance.js +26 -0
- package/dist/commands/performance.js.map +1 -0
- package/dist/commands/press_key.d.ts +10 -0
- package/dist/commands/press_key.d.ts.map +1 -0
- package/dist/commands/press_key.js +103 -0
- package/dist/commands/press_key.js.map +1 -0
- package/dist/commands/scroll.d.ts +21 -0
- package/dist/commands/scroll.d.ts.map +1 -0
- package/dist/commands/scroll.js +54 -0
- package/dist/commands/scroll.js.map +1 -0
- package/dist/commands/select.d.ts +18 -0
- package/dist/commands/select.d.ts.map +1 -0
- package/dist/commands/select.js +53 -0
- package/dist/commands/select.js.map +1 -0
- package/dist/commands/snapshot.d.ts +9 -0
- package/dist/commands/snapshot.d.ts.map +1 -0
- package/dist/commands/snapshot.js +7 -0
- package/dist/commands/snapshot.js.map +1 -0
- package/dist/commands/styles.d.ts +13 -0
- package/dist/commands/styles.d.ts.map +1 -0
- package/dist/commands/styles.js +7 -0
- package/dist/commands/styles.js.map +1 -0
- package/dist/commands/type.d.ts +3 -1
- package/dist/commands/type.d.ts.map +1 -1
- package/dist/commands/type.js +11 -2
- package/dist/commands/type.js.map +1 -1
- package/dist/commands/upload.d.ts +12 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +13 -0
- package/dist/commands/upload.js.map +1 -0
- package/dist/mcp/console-buffer.d.ts +9 -0
- package/dist/mcp/console-buffer.d.ts.map +1 -1
- package/dist/mcp/console-buffer.js.map +1 -1
- package/dist/mcp/index.js +20 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +63 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/browser.d.ts.map +1 -1
- package/dist/mcp/tools/browser.js +109 -11
- package/dist/mcp/tools/browser.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts +5 -0
- package/dist/mcp/tools/connect.d.ts.map +1 -0
- package/dist/mcp/tools/connect.js +48 -0
- package/dist/mcp/tools/connect.js.map +1 -0
- package/dist/mcp/tools/files.d.ts +4 -0
- package/dist/mcp/tools/files.d.ts.map +1 -0
- package/dist/mcp/tools/files.js +85 -0
- package/dist/mcp/tools/files.js.map +1 -0
- package/dist/mcp/tools/interaction.d.ts +4 -0
- package/dist/mcp/tools/interaction.d.ts.map +1 -0
- package/dist/mcp/tools/interaction.js +176 -0
- package/dist/mcp/tools/interaction.js.map +1 -0
- package/dist/mcp/tools/names.d.ts +1 -1
- package/dist/mcp/tools/names.d.ts.map +1 -1
- package/dist/mcp/tools/names.js +19 -1
- package/dist/mcp/tools/names.js.map +1 -1
- package/dist/mcp/tools/network.d.ts +4 -0
- package/dist/mcp/tools/network.d.ts.map +1 -0
- package/dist/mcp/tools/network.js +68 -0
- package/dist/mcp/tools/network.js.map +1 -0
- package/dist/mcp/tools/performance.d.ts +4 -0
- package/dist/mcp/tools/performance.d.ts.map +1 -0
- package/dist/mcp/tools/performance.js +26 -0
- package/dist/mcp/tools/performance.js.map +1 -0
- package/dist/mcp/tools/runtime.d.ts +4 -0
- package/dist/mcp/tools/runtime.d.ts.map +1 -0
- package/dist/mcp/tools/runtime.js +128 -0
- package/dist/mcp/tools/runtime.js.map +1 -0
- package/dist/mcp/tools/snapshot.d.ts +4 -0
- package/dist/mcp/tools/snapshot.d.ts.map +1 -0
- package/dist/mcp/tools/snapshot.js +42 -0
- package/dist/mcp/tools/snapshot.js.map +1 -0
- package/dist/mcp/tools/styles.d.ts +4 -0
- package/dist/mcp/tools/styles.d.ts.map +1 -0
- package/dist/mcp/tools/styles.js +27 -0
- package/dist/mcp/tools/styles.js.map +1 -0
- package/dist/util/config.d.ts.map +1 -1
- package/dist/util/config.js +3 -2
- package/dist/util/config.js.map +1 -1
- package/package.json +5 -4
- package/skills/pilot/SKILL.md +48 -0
- package/src/browser/browser-manager.ts +465 -0
- package/src/browser/chrome-launcher.ts +282 -0
- package/src/browser/connection.ts +84 -0
- package/src/browser/events.ts +121 -0
- package/src/browser/index.ts +20 -0
- package/src/browser/inspect/element-ref.ts +68 -0
- package/src/browser/inspect/snapshot.ts +348 -0
- package/src/browser/inspect/styles.ts +258 -0
- package/src/browser/inspect/types.ts +37 -0
- package/src/browser/interaction/dialogs.ts +54 -0
- package/src/browser/interaction/downloads.ts +116 -0
- package/src/browser/interaction/events.ts +79 -0
- package/src/browser/interaction/ref-resolver.ts +53 -0
- package/src/browser/interaction/uploads.ts +60 -0
- package/src/browser/network/network-buffer.ts +133 -0
- package/src/browser/network/network-monitor.ts +107 -0
- package/src/browser/network/redaction.ts +42 -0
- package/src/browser/network/types.ts +47 -0
- package/src/browser/performance/analyze.ts +298 -0
- package/src/browser/performance/trace.ts +60 -0
- package/src/browser/performance/types.ts +34 -0
- package/src/browser/preflight.ts +144 -0
- package/src/browser/security/origins.ts +102 -0
- package/src/browser/tabs.ts +110 -0
- package/src/browser/types.ts +32 -0
- package/src/browser.ts +22 -798
- package/src/cli/init.ts +40 -49
- package/src/cli/linux-check.ts +23 -0
- package/src/cli/skill-template.ts +4 -113
- package/src/commands/check.ts +59 -0
- package/src/commands/click.ts +18 -4
- package/src/commands/connect.ts +40 -0
- package/src/commands/dialog.ts +79 -0
- package/src/commands/downloads.ts +111 -0
- package/src/commands/fill.ts +34 -0
- package/src/commands/find.ts +52 -0
- package/src/commands/hover.ts +42 -0
- package/src/commands/navigate.ts +20 -0
- package/src/commands/network.ts +141 -0
- package/src/commands/performance.ts +51 -0
- package/src/commands/press_key.ts +134 -0
- package/src/commands/scroll.ts +79 -0
- package/src/commands/select.ts +78 -0
- package/src/commands/snapshot.ts +18 -0
- package/src/commands/styles.ts +24 -0
- package/src/commands/type.ts +12 -2
- package/src/commands/upload.ts +36 -0
- package/src/mcp/console-buffer.ts +10 -0
- package/src/mcp/index.ts +24 -2
- package/src/mcp/server.ts +74 -2
- package/src/mcp/tools/browser.ts +125 -9
- package/src/mcp/tools/connect.ts +62 -0
- package/src/mcp/tools/files.ts +120 -0
- package/src/mcp/tools/interaction.ts +232 -0
- package/src/mcp/tools/names.ts +19 -1
- package/src/mcp/tools/network.ts +86 -0
- package/src/mcp/tools/performance.ts +40 -0
- package/src/mcp/tools/runtime.ts +164 -0
- package/src/mcp/tools/snapshot.ts +64 -0
- package/src/mcp/tools/styles.ts +41 -0
- 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
|
+
}
|