iframer-cli 1.0.2 → 1.0.4
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/cli.cjs +105 -15
- package/index.js +116 -3
- package/package.json +3 -2
package/cli.cjs
CHANGED
|
@@ -201,20 +201,6 @@ const [,, command, ...args] = process.argv;
|
|
|
201
201
|
|
|
202
202
|
async function main() {
|
|
203
203
|
switch (command) {
|
|
204
|
-
case "install": {
|
|
205
|
-
try {
|
|
206
|
-
execSync("claude mcp add iframer -- npx -y --package=iframer-cli iframer-mcp", { stdio: "inherit" });
|
|
207
|
-
console.log("\n iframer MCP installed in Claude Code!");
|
|
208
|
-
console.log(" Now run: iframer login");
|
|
209
|
-
} catch {
|
|
210
|
-
console.error(" Failed to install. Make sure Claude Code CLI is available.");
|
|
211
|
-
console.error(" You can manually add it with:");
|
|
212
|
-
console.error(" claude mcp add iframer -- npx -y --package=iframer-cli iframer-mcp");
|
|
213
|
-
process.exit(1);
|
|
214
|
-
}
|
|
215
|
-
break;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
204
|
case "login":
|
|
219
205
|
await login();
|
|
220
206
|
break;
|
|
@@ -418,6 +404,111 @@ async function main() {
|
|
|
418
404
|
break;
|
|
419
405
|
}
|
|
420
406
|
|
|
407
|
+
// ─── Proxy tunnel ───────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
case "proxy": {
|
|
410
|
+
const token = requireToken();
|
|
411
|
+
const server = getServer();
|
|
412
|
+
const wsUrl = server.replace(/^http/, "ws") + "/proxy/tunnel?token=" + token;
|
|
413
|
+
|
|
414
|
+
let WebSocket;
|
|
415
|
+
try {
|
|
416
|
+
WebSocket = require("ws");
|
|
417
|
+
} catch {
|
|
418
|
+
console.error(" ws package required. Run: npm install -g ws");
|
|
419
|
+
console.error(" Or: bun add ws");
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const net = require("net");
|
|
424
|
+
const ws = new WebSocket(wsUrl);
|
|
425
|
+
const connections = new Map();
|
|
426
|
+
|
|
427
|
+
console.log("\n Connecting to server...");
|
|
428
|
+
|
|
429
|
+
ws.on("open", () => {
|
|
430
|
+
console.log(" Connected! Your browser sessions will route through this machine.");
|
|
431
|
+
console.log(" Keep this running. Press Ctrl+C to stop.\n");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
ws.on("message", (msg) => {
|
|
435
|
+
if (Buffer.isBuffer(msg) && msg.length > 4) {
|
|
436
|
+
// Binary data frame: first 4 bytes = connId
|
|
437
|
+
const connId = msg.readUInt32BE(0);
|
|
438
|
+
const data = msg.slice(4);
|
|
439
|
+
const socket = connections.get(connId);
|
|
440
|
+
if (socket && !socket.destroyed) {
|
|
441
|
+
socket.write(data);
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const data = JSON.parse(msg.toString());
|
|
448
|
+
|
|
449
|
+
if (data.type === "ready") {
|
|
450
|
+
console.log(` Proxy tunnel active (server port ${data.port})`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (data.type === "connect") {
|
|
454
|
+
const { id, host, port } = data;
|
|
455
|
+
const socket = net.createConnection({ host, port }, () => {
|
|
456
|
+
ws.send(JSON.stringify({ type: "connected", id }));
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
socket.on("data", (chunk) => {
|
|
460
|
+
const header = Buffer.alloc(4);
|
|
461
|
+
header.writeUInt32BE(id);
|
|
462
|
+
if (ws.readyState === 1) ws.send(Buffer.concat([header, chunk]));
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
socket.on("end", () => {
|
|
466
|
+
ws.send(JSON.stringify({ type: "close", id }));
|
|
467
|
+
connections.delete(id);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
socket.on("error", (err) => {
|
|
471
|
+
ws.send(JSON.stringify({ type: "error", id, message: err.message }));
|
|
472
|
+
connections.delete(id);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
connections.set(id, socket);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (data.type === "close") {
|
|
479
|
+
const socket = connections.get(data.id);
|
|
480
|
+
if (socket) {
|
|
481
|
+
socket.destroy();
|
|
482
|
+
connections.delete(data.id);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
} catch {}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
ws.on("close", () => {
|
|
489
|
+
console.log("\n Disconnected from server.");
|
|
490
|
+
process.exit(0);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
ws.on("error", (err) => {
|
|
494
|
+
console.error(` Connection error: ${err.message}`);
|
|
495
|
+
process.exit(1);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
process.on("SIGINT", () => {
|
|
499
|
+
console.log("\n Stopping proxy tunnel...");
|
|
500
|
+
for (const [, socket] of connections) {
|
|
501
|
+
try { socket.destroy(); } catch {}
|
|
502
|
+
}
|
|
503
|
+
ws.close();
|
|
504
|
+
process.exit(0);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Block forever
|
|
508
|
+
await new Promise(() => {});
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
|
|
421
512
|
// ─── Screenshot ────────────────────────────────────────────────
|
|
422
513
|
|
|
423
514
|
case "screenshot": {
|
|
@@ -532,7 +623,6 @@ async function main() {
|
|
|
532
623
|
iframer - CLI for the Agentic Browser API
|
|
533
624
|
|
|
534
625
|
Commands:
|
|
535
|
-
install Install iframer MCP in Claude Code
|
|
536
626
|
login Authenticate with the server
|
|
537
627
|
logout Remove saved credentials
|
|
538
628
|
status Show current auth status
|
package/index.js
CHANGED
|
@@ -75,8 +75,13 @@ WORKFLOW:
|
|
|
75
75
|
6. For logins: check credentials with "status". If credentials are missing, call "store_credentials" to prompt the user — NEVER tell them to use the CLI. Then use "login" tool.
|
|
76
76
|
7. Always call "interactive_stop" when done to save session state.
|
|
77
77
|
|
|
78
|
+
CAPTCHA STRATEGY (escalation order):
|
|
79
|
+
1. First try headless — many sites use invisible reCAPTCHA v3 that passes without a challenge.
|
|
80
|
+
2. If a challenge appears, use recaptcha_solve + recaptcha_answer to solve it visually.
|
|
81
|
+
3. If challenges keep looping (3+ attempts), the server IP is flagged. Tell the user to run "iframer proxy" in a separate terminal to route traffic through their residential IP. This is the ONLY CLI command you may suggest. Check proxy status with the "status" tool.
|
|
82
|
+
|
|
78
83
|
MINIMIZE TOKEN USAGE: Don't screenshot every action. Only look when you need to decide what to do next.
|
|
79
|
-
NEVER use bash/CLI
|
|
84
|
+
NEVER use bash/CLI (except "iframer proxy" when CAPTCHA fails). ALL interaction goes through these MCP tools.`,
|
|
80
85
|
});
|
|
81
86
|
|
|
82
87
|
// ─── Tool 0: status (call this first) ────────────────────────────────
|
|
@@ -122,6 +127,12 @@ server.tool(
|
|
|
122
127
|
if (credData.ok) status.credentials = credData.domains;
|
|
123
128
|
} catch {}
|
|
124
129
|
|
|
130
|
+
// Check proxy tunnel
|
|
131
|
+
try {
|
|
132
|
+
const proxyData = await apiGet("/proxy/status");
|
|
133
|
+
if (proxyData.ok) status.proxyTunnel = proxyData.active;
|
|
134
|
+
} catch {}
|
|
135
|
+
|
|
125
136
|
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
|
|
126
137
|
} catch (err) {
|
|
127
138
|
return errorResponse(`Error: ${err.message}`);
|
|
@@ -214,7 +225,8 @@ server.tool(
|
|
|
214
225
|
"interactive_act",
|
|
215
226
|
`Send an action to the interactive browser and get a screenshot URL of the result.
|
|
216
227
|
|
|
217
|
-
Actions: click, human-click, human-type, fill, navigate, scroll, wait, evaluate, wait-for-selector, keyboard.
|
|
228
|
+
Actions: click, human-click, human-type, fill, navigate, scroll, wait, evaluate, wait-for-selector, keyboard, type-code.
|
|
229
|
+
type-code: Type a verification code into split digit inputs in one call. Pass the full code as value (e.g. "735948"). Optionally pass selector for the first input (defaults to input[type="tel"]).
|
|
218
230
|
reCAPTCHA: recaptcha-click (click checkbox), recaptcha-select (select tiles by index), recaptcha-verify (click verify), recaptcha-info (get challenge metadata).
|
|
219
231
|
|
|
220
232
|
Screenshots are saved as files and returned as URLs. Use Read tool to view them. For reCAPTCHA, individual tile image URLs are returned.`,
|
|
@@ -222,7 +234,7 @@ Screenshots are saved as files and returned as URLs. Use Read tool to view them.
|
|
|
222
234
|
action: z.enum([
|
|
223
235
|
"click", "human-click", "human-type", "fill",
|
|
224
236
|
"navigate", "scroll", "wait", "evaluate",
|
|
225
|
-
"wait-for-selector", "keyboard",
|
|
237
|
+
"wait-for-selector", "keyboard", "type-code",
|
|
226
238
|
"recaptcha-click", "recaptcha-select", "recaptcha-verify", "recaptcha-info",
|
|
227
239
|
]).describe("Action type to perform"),
|
|
228
240
|
selector: z.string().optional().describe("CSS selector (for click, fill, human-click, human-type, wait-for-selector)"),
|
|
@@ -514,6 +526,107 @@ ALWAYS call this tool when credentials are needed but not stored. NEVER tell the
|
|
|
514
526
|
}
|
|
515
527
|
);
|
|
516
528
|
|
|
529
|
+
// ─── Tool 11: recaptcha_solve ───────────────────────────────────────
|
|
530
|
+
|
|
531
|
+
server.tool(
|
|
532
|
+
"recaptcha_solve",
|
|
533
|
+
`Start solving a reCAPTCHA. Clicks the checkbox and if a challenge appears, returns ALL tile images inline in one response along with the prompt text. Much faster than individual recaptcha-click + screenshot + recaptcha-info calls.
|
|
534
|
+
|
|
535
|
+
If the reCAPTCHA is solved immediately (no challenge), returns solved: true.
|
|
536
|
+
If a challenge appears, returns the prompt and all tile images. Then use recaptcha_answer with the tile indices to complete it.`,
|
|
537
|
+
{},
|
|
538
|
+
async () => {
|
|
539
|
+
try {
|
|
540
|
+
const data = await apiPost("/interactive/act", {
|
|
541
|
+
action: { type: "recaptcha-solve" },
|
|
542
|
+
screenshot: false,
|
|
543
|
+
});
|
|
544
|
+
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
545
|
+
|
|
546
|
+
const result = data.result;
|
|
547
|
+
if (result.solved) {
|
|
548
|
+
return { content: [{ type: "text", text: "reCAPTCHA solved automatically (no challenge needed)." }] };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Build content blocks: prompt text + all tile images
|
|
552
|
+
const content = [];
|
|
553
|
+
content.push({
|
|
554
|
+
type: "text",
|
|
555
|
+
text: `reCAPTCHA Challenge: "${result.prompt}"\nGrid: ${result.rows}x${result.cols} (${result.tiles.length} tiles)\nSelect the correct tiles by index (0-${result.tiles.length - 1}) using recaptcha_answer.`,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
for (const tile of result.tiles) {
|
|
559
|
+
if (tile.image) {
|
|
560
|
+
content.push({
|
|
561
|
+
type: "text",
|
|
562
|
+
text: `Tile ${tile.index}:`,
|
|
563
|
+
});
|
|
564
|
+
content.push({
|
|
565
|
+
type: "image",
|
|
566
|
+
data: tile.image,
|
|
567
|
+
mimeType: "image/jpeg",
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return { content };
|
|
573
|
+
} catch (err) {
|
|
574
|
+
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
// ─── Tool 12: recaptcha_answer ──────────────────────────────────────
|
|
580
|
+
|
|
581
|
+
server.tool(
|
|
582
|
+
"recaptcha_answer",
|
|
583
|
+
`Submit tile selections and verify the reCAPTCHA in one call. Selects the specified tiles and clicks verify.
|
|
584
|
+
|
|
585
|
+
If solved, returns success. If a new challenge appears (common with reCAPTCHA), returns the new prompt and tile images inline — just call recaptcha_answer again with new selections.`,
|
|
586
|
+
{
|
|
587
|
+
tiles: z.array(z.number()).describe("Tile indices to select (e.g. [0, 3, 6])"),
|
|
588
|
+
},
|
|
589
|
+
async ({ tiles }) => {
|
|
590
|
+
try {
|
|
591
|
+
const data = await apiPost("/interactive/act", {
|
|
592
|
+
action: { type: "recaptcha-answer", tiles },
|
|
593
|
+
screenshot: false,
|
|
594
|
+
});
|
|
595
|
+
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
596
|
+
|
|
597
|
+
const result = data.result;
|
|
598
|
+
if (result.solved) {
|
|
599
|
+
return { content: [{ type: "text", text: "reCAPTCHA solved!" }] };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// New challenge — return tiles inline
|
|
603
|
+
const content = [];
|
|
604
|
+
content.push({
|
|
605
|
+
type: "text",
|
|
606
|
+
text: `Not solved yet — new challenge: "${result.prompt || "unknown"}"\nGrid: ${result.rows}x${result.cols} (${result.tiles.length} tiles)\nSelect the correct tiles by index using recaptcha_answer again.`,
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
for (const tile of result.tiles) {
|
|
610
|
+
if (tile.image) {
|
|
611
|
+
content.push({
|
|
612
|
+
type: "text",
|
|
613
|
+
text: `Tile ${tile.index}:`,
|
|
614
|
+
});
|
|
615
|
+
content.push({
|
|
616
|
+
type: "image",
|
|
617
|
+
data: tile.image,
|
|
618
|
+
mimeType: "image/jpeg",
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return { content };
|
|
624
|
+
} catch (err) {
|
|
625
|
+
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
|
|
517
630
|
// ─── Start ───────────────────────────────────────────────────────────
|
|
518
631
|
|
|
519
632
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iframer-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "CLI and MCP server for iframer — a headful/headless browser for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"url": "https://github.com/EduardoFazolo/iframer-agentic-browser"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
29
|
+
"ws": "^8.20.0"
|
|
29
30
|
}
|
|
30
31
|
}
|