fastbrowser_cli 1.0.35 → 1.0.39
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/README.md +26 -5
- package/dist/contribs/_shared/fastbrowser_helper.d.ts +13 -0
- package/dist/contribs/_shared/fastbrowser_helper.d.ts.map +1 -0
- package/dist/contribs/_shared/fastbrowser_helper.js +39 -0
- package/dist/contribs/_shared/fastbrowser_helper.js.map +1 -0
- package/dist/contribs/linkedin_cli/src/cli.d.ts +3 -0
- package/dist/contribs/linkedin_cli/src/cli.d.ts.map +1 -0
- package/dist/contribs/linkedin_cli/src/cli.js +299 -0
- package/dist/contribs/linkedin_cli/src/cli.js.map +1 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_profile_helper.d.ts +73 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_profile_helper.d.ts.map +1 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_profile_helper.js +866 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_profile_helper.js.map +1 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.d.ts +61 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.d.ts.map +1 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.js +885 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.js.map +1 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_thread_helper.d.ts +11 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_thread_helper.d.ts.map +1 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_thread_helper.js +145 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_thread_helper.js.map +1 -0
- package/dist/contribs/twitter_cli/src/cli.d.ts +3 -0
- package/dist/contribs/twitter_cli/src/cli.d.ts.map +1 -0
- package/dist/contribs/twitter_cli/src/cli.js +273 -0
- package/dist/contribs/twitter_cli/src/cli.js.map +1 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_profile_helper.d.ts +28 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_profile_helper.d.ts.map +1 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_profile_helper.js +274 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_profile_helper.js.map +1 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.d.ts +43 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.d.ts.map +1 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.js +519 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.js.map +1 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_thread_helper.d.ts +11 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_thread_helper.d.ts.map +1 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_thread_helper.js +213 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_thread_helper.js.map +1 -0
- package/dist/fastbrowser_cli/fastbrowser_cli.js +43 -0
- package/dist/fastbrowser_cli/fastbrowser_cli.js.map +1 -1
- package/dist/fastbrowser_httpd/libs/tool-schemas.d.ts +4 -0
- package/dist/fastbrowser_httpd/libs/tool-schemas.d.ts.map +1 -1
- package/dist/fastbrowser_httpd/libs/tool-schemas.js +4 -0
- package/dist/fastbrowser_httpd/libs/tool-schemas.js.map +1 -1
- package/dist/fastbrowser_mcp/fastbrowser_mcp.js +36 -2
- package/dist/fastbrowser_mcp/fastbrowser_mcp.js.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts +2 -0
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.js +12 -0
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.js.map +1 -1
- package/dist/fastbrowser_mcp/libs/response_formatter.d.ts +1 -0
- package/dist/fastbrowser_mcp/libs/response_formatter.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/libs/response_formatter.js +27 -0
- package/dist/fastbrowser_mcp/libs/response_formatter.js.map +1 -1
- package/dist/shared/fastbrowser_helper.d.ts +13 -0
- package/dist/shared/fastbrowser_helper.d.ts.map +1 -0
- package/dist/shared/fastbrowser_helper.js +39 -0
- package/dist/shared/fastbrowser_helper.js.map +1 -0
- package/examples/linkedin_cli_TOREMOVE/README.md +7 -0
- package/examples/linkedin_cli_TOREMOVE/linkedin_dm.sh +40 -0
- package/examples/linkedin_cli_TOREMOVE/linkedin_dm.ts +326 -0
- package/examples/linkedin_cli_TOREMOVE/linkedin_dm_messages.ts +279 -0
- package/examples/linkedin_cli_TOREMOVE/linkedin_full_cycle.sh +5 -0
- package/examples/{linkedin_cli/linked_post.sh → linkedin_cli_TOREMOVE/linkedin_post.sh} +3 -0
- package/examples/linkedin_cli_TOREMOVE/message_thread.a11y.txt +252 -0
- package/examples/whatsapp/whatapp.a11y.txt +1521 -0
- package/examples/whatsapp/whatsapp.sh +10 -0
- package/listitem +7 -0
- package/package.json +7 -3
- package/skills/fastbrowser/SKILL.md +116 -29
- package/src/contribs/_shared/fastbrowser_helper.ts +49 -0
- package/src/contribs/linkedin_cli/README.md +80 -0
- package/src/contribs/linkedin_cli/data/linkedin_posts_jeromeetienne.a11y.txt +2364 -0
- package/src/contribs/linkedin_cli/data/linkedin_posts_jontwigge.a11y.txt +2740 -0
- package/src/contribs/linkedin_cli/data/linkedin_posts_julien_guezennec.a11y.txt +2073 -0
- package/src/contribs/linkedin_cli/data/linkedin_profile_jeromeetienne.a11y.txt +1863 -0
- package/src/contribs/linkedin_cli/data/linkedin_profile_jontwigge.a11y.txt +1738 -0
- package/src/contribs/linkedin_cli/data/linkedin_profile_julien_guezennec.a11y.txt +2182 -0
- package/src/contribs/linkedin_cli/src/cli.ts +345 -0
- package/src/contribs/linkedin_cli/src/libs/linkedin_profile_helper.ts +964 -0
- package/src/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.ts +982 -0
- package/src/contribs/linkedin_cli/src/libs/linkedin_thread_helper.ts +171 -0
- package/src/contribs/twitter_cli/README.md +79 -0
- package/src/contribs/twitter_cli/data/twitter_chat.a11y.txt +215 -0
- package/src/contribs/twitter_cli/data/twitter_home.a11y.txt +467 -0
- package/src/contribs/twitter_cli/data/twitter_profile.a11y.txt +418 -0
- package/src/contribs/twitter_cli/data/twitter_profile_jontwigge.a11y.txt +484 -0
- package/src/contribs/twitter_cli/data/twitter_profile_molokoloco.a11y.txt +483 -0
- package/src/contribs/twitter_cli/src/cli.ts +315 -0
- package/src/contribs/twitter_cli/src/libs/twitter_profile_helper.ts +328 -0
- package/src/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.ts +607 -0
- package/src/contribs/twitter_cli/src/libs/twitter_thread_helper.ts +240 -0
- package/src/fastbrowser_cli/fastbrowser_cli.ts +51 -0
- package/src/fastbrowser_httpd/libs/tool-schemas.ts +6 -0
- package/src/fastbrowser_mcp/fastbrowser_mcp.ts +46 -3
- package/src/fastbrowser_mcp/libs/mcp_target_helper.ts +11 -0
- package/src/fastbrowser_mcp/libs/response_formatter.ts +29 -0
- package/src/shared/fastbrowser_helper.ts +49 -0
- package/tsconfig.json +1 -1
- package/examples/linkedin_cli/linked_dm.sh +0 -19
- package/examples/mcp_client_playwright.ts +0 -34
- /package/examples/{linkedin_cli → linkedin_cli_TOREMOVE}/linkedin.snapshot.txt +0 -0
- /package/examples/{twitter_cli → twitter_cli_TOREMOVE}/twitter_post.sh +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// npm imports
|
|
2
|
+
import { A11yQuery, A11yTree, AxNode } from 'a11y_parse';
|
|
3
|
+
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
//
|
|
7
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
8
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
9
|
+
|
|
10
|
+
const TIME_OF_DAY_REGEXP = /^\d{1,2}:\d{2}\s*(AM|PM)$/i;
|
|
11
|
+
const FULL_DATE_REGEXP = /^([A-Za-z]+)\s+(\d{1,2})(?:,\s*(\d{4}))?$/;
|
|
12
|
+
const WEEKDAYS: Record<string, number> = {
|
|
13
|
+
Sunday: 0, Monday: 1, Tuesday: 2, Wednesday: 3,
|
|
14
|
+
Thursday: 4, Friday: 5, Saturday: 6,
|
|
15
|
+
};
|
|
16
|
+
const MONTHS: Record<string, number> = {
|
|
17
|
+
Jan: 0, January: 0,
|
|
18
|
+
Feb: 1, February: 1,
|
|
19
|
+
Mar: 2, March: 2,
|
|
20
|
+
Apr: 3, April: 3,
|
|
21
|
+
May: 4,
|
|
22
|
+
Jun: 5, June: 5,
|
|
23
|
+
Jul: 6, July: 6,
|
|
24
|
+
Aug: 7, August: 7,
|
|
25
|
+
Sep: 8, September: 8,
|
|
26
|
+
Oct: 9, October: 9,
|
|
27
|
+
Nov: 10, November: 10,
|
|
28
|
+
Dec: 11, December: 11,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class TwitterThreadHelper {
|
|
32
|
+
static async parseMessagesThread(
|
|
33
|
+
rawOutput: string,
|
|
34
|
+
otherHandle: string,
|
|
35
|
+
overrideYear?: number,
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
const treeText = TwitterThreadHelper.extractAxTreeText(rawOutput);
|
|
38
|
+
if (treeText.length === 0) {
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
const root = A11yTree.parse(treeText);
|
|
42
|
+
const items = A11yQuery.querySelectorAll(root, 'main listitem');
|
|
43
|
+
const year = overrideYear !== undefined ? overrideYear : new Date().getFullYear();
|
|
44
|
+
|
|
45
|
+
const lines: string[] = [];
|
|
46
|
+
let currentDate: Date | null = null;
|
|
47
|
+
const pending: string[] = [];
|
|
48
|
+
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
const valueGenerics = A11yQuery.querySelectorAll(item, 'generic[value]');
|
|
51
|
+
if (valueGenerics.length === 0) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const dateMarker = TwitterThreadHelper.detectDateMarker(item, valueGenerics, year);
|
|
56
|
+
if (dateMarker !== null) {
|
|
57
|
+
currentDate = dateMarker;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const timestampNodes = TwitterThreadHelper.findTimestampNodes(valueGenerics);
|
|
62
|
+
if (timestampNodes.length === 0) {
|
|
63
|
+
const text = TwitterThreadHelper.collectText(valueGenerics, new Set());
|
|
64
|
+
if (text.length > 0) {
|
|
65
|
+
pending.push(text);
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const time = TwitterThreadHelper.parseTimeOfDay(timestampNodes[0].attributes['value']);
|
|
71
|
+
if (time === null) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (currentDate === null) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sender = A11yQuery.querySelector(item, 'img') !== undefined
|
|
79
|
+
? 'You'
|
|
80
|
+
: otherHandle;
|
|
81
|
+
|
|
82
|
+
const timestampUids = new Set(timestampNodes.map((n) => n.uid));
|
|
83
|
+
const text = TwitterThreadHelper.collectText(valueGenerics, timestampUids);
|
|
84
|
+
const iso = TwitterThreadHelper.combineDateTime(currentDate, time);
|
|
85
|
+
|
|
86
|
+
for (const pendingText of pending) {
|
|
87
|
+
lines.push(`${iso}:${sender}:${pendingText}`);
|
|
88
|
+
}
|
|
89
|
+
pending.length = 0;
|
|
90
|
+
|
|
91
|
+
if (text.length === 0) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
lines.push(`${iso}:${sender}:${text}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private static collectText(valueGenerics: AxNode[], excludeUids: Set<string>): string {
|
|
101
|
+
const parts: string[] = [];
|
|
102
|
+
for (const node of valueGenerics) {
|
|
103
|
+
if (excludeUids.has(node.uid) === true) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const value = node.attributes['value'];
|
|
107
|
+
if (value === undefined) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const trimmed = value.trim();
|
|
111
|
+
if (trimmed.length === 0) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
parts.push(trimmed);
|
|
115
|
+
}
|
|
116
|
+
return parts.join(' ');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
120
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
121
|
+
//
|
|
122
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
123
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
124
|
+
|
|
125
|
+
private static extractAxTreeText(rawOutput: string): string {
|
|
126
|
+
const lines: string[] = [];
|
|
127
|
+
for (const line of rawOutput.split('\n')) {
|
|
128
|
+
if (/^\s*uid=/.test(line) === true) {
|
|
129
|
+
lines.push(line);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private static findTimestampNodes(valueGenerics: AxNode[]): AxNode[] {
|
|
136
|
+
const result: AxNode[] = [];
|
|
137
|
+
for (const node of valueGenerics) {
|
|
138
|
+
const value = node.attributes['value'];
|
|
139
|
+
if (value === undefined) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (TIME_OF_DAY_REGEXP.test(value) === true) {
|
|
143
|
+
result.push(node);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private static detectDateMarker(
|
|
150
|
+
item: AxNode,
|
|
151
|
+
valueGenerics: AxNode[],
|
|
152
|
+
fallbackYear: number,
|
|
153
|
+
): Date | null {
|
|
154
|
+
const hasTimestamp = TwitterThreadHelper.findTimestampNodes(valueGenerics).length > 0;
|
|
155
|
+
if (hasTimestamp === true) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
if (A11yQuery.querySelector(item, 'img') !== undefined) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
for (const node of valueGenerics) {
|
|
162
|
+
const raw = node.attributes['value'];
|
|
163
|
+
if (raw === undefined) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const value = raw.trim();
|
|
167
|
+
const parsed = TwitterThreadHelper.parseDateMarker(value, fallbackYear);
|
|
168
|
+
if (parsed !== null) {
|
|
169
|
+
return parsed;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private static parseDateMarker(value: string, fallbackYear: number): Date | null {
|
|
176
|
+
if (value === 'Today') {
|
|
177
|
+
const now = new Date();
|
|
178
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
179
|
+
}
|
|
180
|
+
if (value === 'Yesterday') {
|
|
181
|
+
const now = new Date();
|
|
182
|
+
const d = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
183
|
+
d.setDate(d.getDate() - 1);
|
|
184
|
+
return d;
|
|
185
|
+
}
|
|
186
|
+
if (WEEKDAYS[value] !== undefined) {
|
|
187
|
+
const target = WEEKDAYS[value];
|
|
188
|
+
const now = new Date();
|
|
189
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
190
|
+
const diff = (today.getDay() - target + 7) % 7;
|
|
191
|
+
const days = diff === 0 ? 7 : diff;
|
|
192
|
+
today.setDate(today.getDate() - days);
|
|
193
|
+
return today;
|
|
194
|
+
}
|
|
195
|
+
const match = value.match(FULL_DATE_REGEXP);
|
|
196
|
+
if (match === null) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const monthIndex = MONTHS[match[1]];
|
|
200
|
+
if (monthIndex === undefined) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const day = parseInt(match[2], 10);
|
|
204
|
+
const explicitYear = match[3] !== undefined ? parseInt(match[3], 10) : null;
|
|
205
|
+
const year = explicitYear !== null ? explicitYear : fallbackYear;
|
|
206
|
+
const candidate = new Date(year, monthIndex, day);
|
|
207
|
+
if (explicitYear === null && candidate.getTime() > Date.now()) {
|
|
208
|
+
candidate.setFullYear(year - 1);
|
|
209
|
+
}
|
|
210
|
+
return candidate;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private static parseTimeOfDay(value: string | undefined): { hours: number; minutes: number } | null {
|
|
214
|
+
if (value === undefined) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const match = value.trim().match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
|
|
218
|
+
if (match === null) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
let hours = parseInt(match[1], 10);
|
|
222
|
+
const minutes = parseInt(match[2], 10);
|
|
223
|
+
const meridiem = match[3].toUpperCase();
|
|
224
|
+
if (meridiem === 'AM' && hours === 12) {
|
|
225
|
+
hours = 0;
|
|
226
|
+
} else if (meridiem === 'PM' && hours !== 12) {
|
|
227
|
+
hours += 12;
|
|
228
|
+
}
|
|
229
|
+
return { hours, minutes };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private static combineDateTime(date: Date, time: { hours: number; minutes: number }): string {
|
|
233
|
+
const yyyy = date.getFullYear().toString().padStart(4, '0');
|
|
234
|
+
const mm = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
235
|
+
const dd = date.getDate().toString().padStart(2, '0');
|
|
236
|
+
const hh = time.hours.toString().padStart(2, '0');
|
|
237
|
+
const mi = time.minutes.toString().padStart(2, '0');
|
|
238
|
+
return `${yyyy}-${mm}-${dd}T${hh}:${mi}:00`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -252,6 +252,48 @@ async function main(): Promise<void> {
|
|
|
252
252
|
await MainHelper.runTool(cmd, 'list_pages', {});
|
|
253
253
|
});
|
|
254
254
|
|
|
255
|
+
program
|
|
256
|
+
.command('check')
|
|
257
|
+
.description('Verify the connection with the browser; restart the server once if it looks desynched')
|
|
258
|
+
.action(async (_opts, cmd: Command) => {
|
|
259
|
+
const serverUrl = MainHelper.getServerUrlFromCmd(cmd);
|
|
260
|
+
const mcpTarget = MainHelper.getMcpTargetFromCmd(cmd);
|
|
261
|
+
|
|
262
|
+
const hasPages = (response: { content: Array<{ text: string }> }): boolean => {
|
|
263
|
+
const joined = response.content.map((part) => part.text).join('\n');
|
|
264
|
+
const lines = joined.split('\n');
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
if (/^\s*\d+:/.test(line) === true) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return false;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
if (MainHelper.getAutostartFromCmd(cmd) === true) {
|
|
274
|
+
await ServerManager.ensureRunning(serverUrl, mcpTarget);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const firstResponse = await HttpClient.postTool(serverUrl, 'list_pages', {});
|
|
278
|
+
if (hasPages(firstResponse) === true) {
|
|
279
|
+
console.log('It is properly connected to the browser');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
console.log('Connection with the browser is desynched, relaunching the server');
|
|
284
|
+
await ServerManager.stop(serverUrl);
|
|
285
|
+
await ServerManager.start(serverUrl, mcpTarget);
|
|
286
|
+
|
|
287
|
+
const secondResponse = await HttpClient.postTool(serverUrl, 'list_pages', {});
|
|
288
|
+
if (hasPages(secondResponse) === true) {
|
|
289
|
+
console.log('It is properly connected to the browser');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.error('Connection with the browser is still broken after server restart');
|
|
294
|
+
process.exit(1);
|
|
295
|
+
});
|
|
296
|
+
|
|
255
297
|
program
|
|
256
298
|
.command('new_page')
|
|
257
299
|
.description('Open a new browser page')
|
|
@@ -343,6 +385,15 @@ async function main(): Promise<void> {
|
|
|
343
385
|
await MainHelper.runTool(cmd, 'press_keys', { keys: opts.keys });
|
|
344
386
|
});
|
|
345
387
|
|
|
388
|
+
program
|
|
389
|
+
.command('evaluate_script [file]')
|
|
390
|
+
.description('Evaluate a JavaScript function in the page context. Provide the function text inline via --script, as a [file] path, or piped on stdin. The function should return JSON-able data.')
|
|
391
|
+
.option('--script <script>', 'Inline JS function text (overrides [file] and stdin)')
|
|
392
|
+
.action(async (file: string | undefined, opts: { script?: string }, cmd: Command) => {
|
|
393
|
+
const functionTxt = await MainHelper.readBatchSource(file, opts.script);
|
|
394
|
+
await MainHelper.runTool(cmd, 'evaluate_script', { functionTxt });
|
|
395
|
+
});
|
|
396
|
+
|
|
346
397
|
program
|
|
347
398
|
.command('install [skill-folder]')
|
|
348
399
|
.description('Install all bundled skills into <skill-folder>/skills/ (default: .)')
|
|
@@ -61,6 +61,11 @@ export type PressKeysRequest = z.infer<typeof PressKeysRequestSchema>;
|
|
|
61
61
|
export const TakeSnapshotRequestSchema = z.object({}).strict();
|
|
62
62
|
export type TakeSnapshotRequest = z.infer<typeof TakeSnapshotRequestSchema>;
|
|
63
63
|
|
|
64
|
+
export const EvaluateScriptRequestSchema = z.object({
|
|
65
|
+
functionTxt: z.string().describe('JS function text to evaluate in the page context. Should return JSON-able data.'),
|
|
66
|
+
});
|
|
67
|
+
export type EvaluateScriptRequest = z.infer<typeof EvaluateScriptRequestSchema>;
|
|
68
|
+
|
|
64
69
|
///////////////////////////////////////////////////////////////////////////////
|
|
65
70
|
///////////////////////////////////////////////////////////////////////////////
|
|
66
71
|
// Uniform response — narrowed mirror of MCP's CallToolResult
|
|
@@ -104,4 +109,5 @@ export const TOOL_SCHEMAS: ToolSchemaEntry[] = [
|
|
|
104
109
|
{ routeName: 'query_selectors', mcpToolName: 'querySelectors', requestSchema: QuerySelectorRequestSchema },
|
|
105
110
|
{ routeName: 'press_keys', mcpToolName: 'pressKeys', requestSchema: PressKeysRequestSchema },
|
|
106
111
|
{ routeName: 'take_snapshot', mcpToolName: 'take_snapshot', requestSchema: TakeSnapshotRequestSchema },
|
|
112
|
+
{ routeName: 'evaluate_script', mcpToolName: 'evaluate_script', requestSchema: EvaluateScriptRequestSchema },
|
|
107
113
|
];
|
|
@@ -504,7 +504,20 @@ class MainHelper {
|
|
|
504
504
|
const keysToSend: string[] = [];
|
|
505
505
|
const keysSplit = keys.split(',').map((key) => key.trim());
|
|
506
506
|
for (const key of keysSplit) {
|
|
507
|
-
|
|
507
|
+
// from chatgpt
|
|
508
|
+
const specialKeys = [
|
|
509
|
+
// Control / navigation
|
|
510
|
+
'Enter', 'Tab', 'Escape', 'Backspace', 'Delete', 'Insert',
|
|
511
|
+
|
|
512
|
+
// Arrows
|
|
513
|
+
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
|
|
514
|
+
|
|
515
|
+
// Navigation keys
|
|
516
|
+
'Home', 'End', 'PageUp', 'PageDown',
|
|
517
|
+
|
|
518
|
+
// Function keys
|
|
519
|
+
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
|
|
520
|
+
];
|
|
508
521
|
if (specialKeys.includes(key)) {
|
|
509
522
|
keysToSend.push(key);
|
|
510
523
|
} else {
|
|
@@ -514,7 +527,7 @@ class MainHelper {
|
|
|
514
527
|
}
|
|
515
528
|
}
|
|
516
529
|
}
|
|
517
|
-
console.error("Keys to send:", keysToSend);
|
|
530
|
+
// console.error("Keys to send:", keysToSend);
|
|
518
531
|
// chrome-devtools-mcp's 'press_key' tool accepts a single 'key' per call — loop through the sequence
|
|
519
532
|
for (const key of keysToSend) {
|
|
520
533
|
const toolConfig = await McpTargetHelper.targetToolPressKey(mcpTarget, key);
|
|
@@ -569,7 +582,7 @@ class MainHelper {
|
|
|
569
582
|
|
|
570
583
|
// log the events
|
|
571
584
|
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.click}: output:`);
|
|
572
|
-
|
|
585
|
+
logger.warn(`${outputText}`);
|
|
573
586
|
return {
|
|
574
587
|
content: [{ type: "text", text: outputText }],
|
|
575
588
|
};
|
|
@@ -641,6 +654,36 @@ class MainHelper {
|
|
|
641
654
|
}
|
|
642
655
|
);
|
|
643
656
|
|
|
657
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
658
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
659
|
+
//
|
|
660
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
661
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
662
|
+
|
|
663
|
+
mcpServer.registerTool(
|
|
664
|
+
McpTargetHelper.EXTERNAL_TOOL_NAME.evaluateScript,
|
|
665
|
+
{
|
|
666
|
+
description: "Evaluate a JavaScript function in the context of the current page. The function should return JSON-able data.",
|
|
667
|
+
inputSchema: z.object({
|
|
668
|
+
functionTxt: z.string().describe("The JavaScript function to evaluate in the page context. Should return JSON-able data."),
|
|
669
|
+
}),
|
|
670
|
+
},
|
|
671
|
+
async ({ functionTxt }: { functionTxt: string }) => {
|
|
672
|
+
// log the events
|
|
673
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.evaluateScript}: evaluating function: ${functionTxt}`);
|
|
674
|
+
|
|
675
|
+
const toolConfig = await McpTargetHelper.targetToolEvaluateScript(mcpTarget, functionTxt);
|
|
676
|
+
const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
|
|
677
|
+
let outputText = await ResponseFormatter.formatEvaluateScript(mcpTarget, callToolResult);
|
|
678
|
+
|
|
679
|
+
// log the events
|
|
680
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.evaluateScript}: output:`);
|
|
681
|
+
logger.warn(`${outputText}`);
|
|
682
|
+
return {
|
|
683
|
+
content: [{ type: "text", text: outputText }],
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
)
|
|
644
687
|
///////////////////////////////////////////////////////////////////////////////
|
|
645
688
|
///////////////////////////////////////////////////////////////////////////////
|
|
646
689
|
// .get_current_datetime tool implementation
|
|
@@ -18,6 +18,7 @@ export class McpTargetHelper {
|
|
|
18
18
|
pressKeys: "pressKeys",
|
|
19
19
|
click: "click",
|
|
20
20
|
fillForm: "fill_form",
|
|
21
|
+
evaluateScript: "evaluate_script",
|
|
21
22
|
getCurrentDateTime: "get_current_datetime",
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -212,4 +213,14 @@ export class McpTargetHelper {
|
|
|
212
213
|
throw new Error(`Unsupported MCP target: ${mcpTarget}`);
|
|
213
214
|
}
|
|
214
215
|
}
|
|
216
|
+
|
|
217
|
+
static async targetToolEvaluateScript(mcpTarget: FastBrowserMcpTarget, functionText: string): Promise<TargetToolConfig> {
|
|
218
|
+
if (mcpTarget === 'chrome_devtools') {
|
|
219
|
+
return { toolName: 'evaluate_script', toolArgs: { function: functionText } };
|
|
220
|
+
} else if (mcpTarget === 'playwright') {
|
|
221
|
+
return { toolName: 'browser_evaluate', toolArgs: { function: functionText } };
|
|
222
|
+
} else {
|
|
223
|
+
throw new Error(`Unsupported MCP target: ${mcpTarget}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
215
226
|
}
|
|
@@ -211,4 +211,33 @@ export class ResponseFormatter {
|
|
|
211
211
|
throw new Error(`Unsupported MCP target: ${mcpTarget}`);
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
|
+
|
|
215
|
+
static async formatEvaluateScript(mcpTarget: FastBrowserMcpTarget, callToolResult: CallToolResult): Promise<string> {
|
|
216
|
+
const resultContent = callToolResult.content[0]
|
|
217
|
+
if (resultContent.type !== "text") throw new Error("Unexpected content type");
|
|
218
|
+
const resultText: string = resultContent.text
|
|
219
|
+
|
|
220
|
+
// ### Result
|
|
221
|
+
// undefined
|
|
222
|
+
// ### Ran Playwright code
|
|
223
|
+
// ```js
|
|
224
|
+
// await page.evaluate('() => {\n const workspace = document.querySelector(\'main#workspace\');\n if (workspace === null) {\n throw new Error(\'Workspace element not found\');\n }\n const tryCount = 6;\n const delayMs = 500;\n (async () => {\n for (let i = 0; i < tryCount; i++) {\n workspace.scrollBy({\n top: 600000,\n behavior: \'smooth\'\n });\n await new Promise(resolve => setTimeout(resolve, delayMs));\n }\n })();\n}');
|
|
225
|
+
// ```
|
|
226
|
+
// ### Events
|
|
227
|
+
// - New console entries: .playwright-mcp/console-2026-05-04T05-43-55-775Z.log#L3959-L4058
|
|
228
|
+
|
|
229
|
+
// Target format example:
|
|
230
|
+
// Successfully clicked on the element
|
|
231
|
+
|
|
232
|
+
if (mcpTarget === 'chrome_devtools') {
|
|
233
|
+
// EXAMPLE:
|
|
234
|
+
// Successfully clicked on the element
|
|
235
|
+
|
|
236
|
+
return resultText
|
|
237
|
+
} else if (mcpTarget === 'playwright') {
|
|
238
|
+
return resultText
|
|
239
|
+
} else {
|
|
240
|
+
throw new Error(`Unsupported MCP target: ${mcpTarget}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
214
243
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const __dirname = new URL('.', import.meta.url).pathname;
|
|
4
|
+
|
|
5
|
+
export class FastBrowserHelper {
|
|
6
|
+
static async run(command: string): Promise<string> {
|
|
7
|
+
// const fullCommand = `npx fastbrowser_cli ${command}`;
|
|
8
|
+
const fullCommand = `npx tsx ${__dirname}../../src/fastbrowser_cli/fastbrowser_cli.ts ${command}`;
|
|
9
|
+
console.error(`Running command: ${fullCommand}`);
|
|
10
|
+
return execSync(fullCommand, { encoding: 'utf8' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static async navigatePage(url: string): Promise<void> {
|
|
14
|
+
await FastBrowserHelper.run(`navigate_page --url '${url}'`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static async fillForm(selector: string, value: string): Promise<void> {
|
|
18
|
+
await FastBrowserHelper.run(`fill_form --selector '${selector}' --value '${value}'`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static async pressKeys(keys: string): Promise<void> {
|
|
22
|
+
await FastBrowserHelper.run(`press_keys --keys '${keys}'`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static async click(selector: string): Promise<void> {
|
|
26
|
+
await FastBrowserHelper.run(`click -s '${selector}'`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static async querySelectorsAll(selector: string, limit: number): Promise<string> {
|
|
30
|
+
return await FastBrowserHelper.run(`query_selectors --all --selector '${selector}' --limit ${limit}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static async querySelectorsAllWithChildren(selector: string, limit: number): Promise<string> {
|
|
34
|
+
return await FastBrowserHelper.run(`query_selectors --all --selector '${selector}' --limit ${limit} --with-ancestors --with-children`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static async takeSnapshot(): Promise<string> {
|
|
38
|
+
return await FastBrowserHelper.run('take_snapshot');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static async querySelectors(selector: string, withAncestors = true): Promise<string> {
|
|
42
|
+
const flag = withAncestors === false ? ' --no-with-ancestors' : '';
|
|
43
|
+
return await FastBrowserHelper.run(`query_selectors --selector '${selector}'${flag}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static async evaluateScript(functionText: string): Promise<string> {
|
|
47
|
+
return await FastBrowserHelper.run(`evaluate_script --script "${functionText}"`);
|
|
48
|
+
}
|
|
49
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# Restart the server to clear any previous state
|
|
4
|
-
NODE_OPTIONS='' NPM_CONFIG_LOGLEVEL=silent npm run dev:cli -- server restart
|
|
5
|
-
|
|
6
|
-
# Goto linkedin messaging page using the CLI commands below:
|
|
7
|
-
NODE_OPTIONS='' NPM_CONFIG_LOGLEVEL=silent npm run dev:cli -- navigate_page --url https://www.linkedin.com/messaging/
|
|
8
|
-
|
|
9
|
-
# list all the threads conversations in the left sidebar
|
|
10
|
-
NODE_OPTIONS='' NPM_CONFIG_LOGLEVEL=silent npm run dev:cli -- query_selectors -s 'list[name="Conversation List"] > listitem heading' -a
|
|
11
|
-
|
|
12
|
-
# Select the conversation with Eric Defiez
|
|
13
|
-
NODE_OPTIONS='' NPM_CONFIG_LOGLEVEL=silent npm run dev:cli -- click -s 'list[name="Conversation List"] > listitem heading[name^="Eric Defiez"]'
|
|
14
|
-
|
|
15
|
-
# Fill the message content
|
|
16
|
-
NODE_OPTIONS='' NPM_CONFIG_LOGLEVEL=silent npm run dev:cli -- fill_form -s 'textbox[name^="Write"]' -v "Hello"
|
|
17
|
-
|
|
18
|
-
# Click the "Send" button to send the message
|
|
19
|
-
NODE_OPTIONS='' NPM_CONFIG_LOGLEVEL=silent npm run dev:cli -- click -s 'button[name^="Send"]'
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
-
|
|
4
|
-
async function main() {
|
|
5
|
-
// Note: the PLAYWRIGHT_MCP_EXTENSION_TOKEN environment variable is required to authenticate with the Playwright MCP extension.
|
|
6
|
-
const client = new Client({
|
|
7
|
-
name: 'foobar',
|
|
8
|
-
version: '0.1.0',
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
const transport = new StdioClientTransport({
|
|
12
|
-
command: 'npx',
|
|
13
|
-
args: ["@playwright/mcp", "--extension"],
|
|
14
|
-
env: {
|
|
15
|
-
PLAYWRIGHT_MCP_EXTENSION_TOKEN: 'd-dwfALmOesZLoS7i-ia8Wf7TWrHtlRMHuVCqAUuiKU'
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
console.log("Connecting to MCP server...");
|
|
19
|
-
await client.connect(transport);
|
|
20
|
-
console.log("Connected!");
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const listToolsResult = await client.listTools()
|
|
24
|
-
console.log("Available tools:", listToolsResult.tools.map(tool => tool.name));
|
|
25
|
-
|
|
26
|
-
// const toolResult = await client.callTool({
|
|
27
|
-
// name: '',
|
|
28
|
-
// arguments: args
|
|
29
|
-
// })
|
|
30
|
-
|
|
31
|
-
await client.close();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
void main();
|
|
File without changes
|
|
File without changes
|