iframer-cli 2.1.2 → 2.1.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.
Files changed (3) hide show
  1. package/cli.js +211 -26
  2. package/mcp-server.cjs +56 -193
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -7,10 +7,182 @@ const { execSync } = require("child_process");
7
7
 
8
8
  const readline = require("readline");
9
9
 
10
+ const net = require("net");
11
+ const { spawn } = require("child_process");
12
+
10
13
  const CONFIG_DIR = path.join(require("os").homedir(), ".iframer");
11
14
  const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
15
+ const PROXY_FILE = path.join(CONFIG_DIR, "proxy.json");
12
16
  const DEFAULT_SERVER = "https://api.iframer.sh";
13
17
 
18
+ // ─── SOCKS5 server ───────────────────────────────────────────────────────────
19
+
20
+ function createSocks5Server() {
21
+ function handleClient(socket) {
22
+ socket.on("error", () => socket.destroy());
23
+ socket.once("data", (greeting) => {
24
+ if (greeting[0] !== 5) { socket.destroy(); return; }
25
+ socket.write(Buffer.from([5, 0]));
26
+ socket.once("data", (req) => {
27
+ if (req[0] !== 5 || req[1] !== 1) { socket.destroy(); return; }
28
+ let host, port;
29
+ const atype = req[3];
30
+ if (atype === 1) {
31
+ host = `${req[4]}.${req[5]}.${req[6]}.${req[7]}`;
32
+ port = req.readUInt16BE(8);
33
+ } else if (atype === 3) {
34
+ const len = req[4];
35
+ host = req.slice(5, 5 + len).toString();
36
+ port = req.readUInt16BE(5 + len);
37
+ } else if (atype === 4) {
38
+ const parts = [];
39
+ for (let i = 0; i < 16; i += 2) parts.push(req.readUInt16BE(4 + i).toString(16));
40
+ host = parts.join(":");
41
+ port = req.readUInt16BE(20);
42
+ } else { socket.destroy(); return; }
43
+ const target = net.connect(port, host, () => {
44
+ const reply = Buffer.alloc(10);
45
+ reply[0] = 5; reply[1] = 0; reply[2] = 0; reply[3] = 1;
46
+ socket.write(reply);
47
+ socket.pipe(target);
48
+ target.pipe(socket);
49
+ });
50
+ target.on("error", () => socket.destroy());
51
+ socket.on("close", () => target.destroy());
52
+ });
53
+ });
54
+ }
55
+ return net.createServer(handleClient);
56
+ }
57
+
58
+ function startSocks5(listenAll) {
59
+ return new Promise((resolve, reject) => {
60
+ const server = createSocks5Server();
61
+ server.listen(0, listenAll ? "0.0.0.0" : "127.0.0.1", () => {
62
+ resolve({ server, port: server.address().port });
63
+ });
64
+ server.on("error", reject);
65
+ });
66
+ }
67
+
68
+ // ─── WebSocket tunnel client ─────────────────────────────────────────────────
69
+
70
+ function startWsTunnel(baseUrl, authToken, localSocksPort) {
71
+ return new Promise((resolve, reject) => {
72
+ if (!globalThis.WebSocket) {
73
+ reject(new Error("Node.js 22+ is required for the home proxy tunnel. Please upgrade Node."));
74
+ return;
75
+ }
76
+ const wsUrl = baseUrl.replace(/^http/, "ws") + "/proxy/tunnel";
77
+ const ws = new globalThis.WebSocket(wsUrl);
78
+ const sockets = new Map();
79
+ let done = false;
80
+
81
+ const fail = (reason) => {
82
+ if (!done) { done = true; reject(new Error(reason)); }
83
+ try { ws.close(); } catch {}
84
+ };
85
+
86
+ ws.onopen = () => {
87
+ try { ws.send(JSON.stringify({ type: "auth", token: authToken })); }
88
+ catch (e) { fail(`Auth failed: ${e.message}`); }
89
+ };
90
+
91
+ ws.onmessage = (event) => {
92
+ try {
93
+ const msg = JSON.parse(event.data);
94
+ if (msg.type === "ready") {
95
+ if (!done) { done = true; resolve({ remotePort: msg.port, ws, sockets }); }
96
+ return;
97
+ }
98
+ if (msg.type === "open") {
99
+ const sock = net.connect(localSocksPort, "127.0.0.1");
100
+ sockets.set(msg.id, sock);
101
+ sock.on("data", (d) => {
102
+ try { if (ws.readyState === 1) ws.send(JSON.stringify({ type: "data", id: msg.id, data: d.toString("base64") })); }
103
+ catch {}
104
+ });
105
+ sock.on("close", () => {
106
+ sockets.delete(msg.id);
107
+ try { if (ws.readyState === 1) ws.send(JSON.stringify({ type: "close", id: msg.id })); } catch {}
108
+ });
109
+ sock.on("error", () => sock.destroy());
110
+ return;
111
+ }
112
+ if (msg.type === "data") { sockets.get(msg.id)?.write(Buffer.from(msg.data, "base64")); return; }
113
+ if (msg.type === "close") { sockets.get(msg.id)?.destroy(); sockets.delete(msg.id); }
114
+ } catch {}
115
+ };
116
+
117
+ ws.onerror = (e) => fail(`WebSocket error: ${e?.message ?? "unknown"}`);
118
+ ws.onclose = (e) => {
119
+ sockets.forEach(s => s.destroy()); sockets.clear();
120
+ fail(`Tunnel closed (code=${e?.code ?? "?"}, reason=${e?.reason || "unknown"})`);
121
+ };
122
+
123
+ setTimeout(() => fail("Tunnel setup timed out (15s)"), 15000);
124
+ });
125
+ }
126
+
127
+ // ─── Proxy daemon ────────────────────────────────────────────────────────────
128
+
129
+ function readProxyState() {
130
+ try {
131
+ const data = JSON.parse(fs.readFileSync(PROXY_FILE, "utf8"));
132
+ try { process.kill(data.pid, 0); return { active: true, ...data }; }
133
+ catch { try { fs.unlinkSync(PROXY_FILE); } catch {} return { active: false }; }
134
+ } catch { return { active: false }; }
135
+ }
136
+
137
+ async function runProxyDaemon() {
138
+ const server = getServer();
139
+ const isLocal = server.includes("localhost") || server.includes("127.0.0.1");
140
+
141
+ let socks, tunnel;
142
+ const cleanup = async () => {
143
+ try { socks?.server.close(); } catch {}
144
+ try { tunnel?.ws.close(); } catch {}
145
+ try {
146
+ const ac = new AbortController();
147
+ setTimeout(() => ac.abort(), 3000);
148
+ await fetch(`${server}/proxy`, {
149
+ method: "POST", headers: { "Content-Type": "application/json" },
150
+ body: JSON.stringify({ server: null }),
151
+ signal: ac.signal,
152
+ });
153
+ } catch {}
154
+ try { fs.unlinkSync(PROXY_FILE); } catch {}
155
+ };
156
+
157
+ try {
158
+ socks = await startSocks5(true); // listen on 0.0.0.0 so Docker can reach it
159
+
160
+ let proxyUrl;
161
+ if (isLocal) {
162
+ proxyUrl = `socks5://host.docker.internal:${socks.port}`;
163
+ } else {
164
+ const token = loadToken();
165
+ tunnel = await startWsTunnel(server, token, socks.port);
166
+ proxyUrl = `socks5://127.0.0.1:${tunnel.remotePort}`;
167
+ }
168
+
169
+ await fetch(`${server}/proxy`, {
170
+ method: "POST", headers: { "Content-Type": "application/json" },
171
+ body: JSON.stringify({ server: proxyUrl }),
172
+ });
173
+
174
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
175
+ fs.writeFileSync(PROXY_FILE, JSON.stringify({ pid: process.pid, proxyUrl, startedAt: new Date().toISOString() }, null, 2));
176
+
177
+ await new Promise((resolve) => {
178
+ process.once("SIGTERM", resolve);
179
+ process.once("SIGINT", resolve);
180
+ });
181
+ } finally {
182
+ await cleanup();
183
+ }
184
+ }
185
+
14
186
  function getServer() {
15
187
  return process.env.IFRAMER_URL || DEFAULT_SERVER;
16
188
  }
@@ -518,38 +690,51 @@ async function main() {
518
690
 
519
691
  case "proxy": {
520
692
  const action = args[0];
693
+ const isDaemon = args.includes("--daemon");
694
+ const isForeground = args.includes("--foreground");
695
+
521
696
  if (!action || !["start", "stop", "status"].includes(action)) {
522
- console.log(" Usage: iframer proxy <start|stop|status> [proxy-url]");
523
- console.log(" Note: 'start' requires a proxy URL (e.g. socks5://host:port).");
524
- console.log(" To use your home IP automatically, use the iframer MCP tool instead.");
697
+ console.log(" Usage: iframer proxy <start|stop|status>");
698
+ console.log(" start Route browser traffic through your home IP (runs in foreground)");
699
+ console.log(" stop Stop the home proxy");
700
+ console.log(" status Check proxy status");
525
701
  break;
526
702
  }
703
+
527
704
  if (action === "status") {
528
- const res = await fetch(`${getServer()}/proxy`);
529
- const data = await res.json();
530
- console.log(JSON.stringify(data, null, 2));
531
- } else if (action === "stop") {
532
- const res = await fetch(`${getServer()}/proxy`, {
533
- method: "POST",
534
- headers: { "Content-Type": "application/json" },
535
- body: JSON.stringify({ server: null }),
536
- });
537
- const data = await res.json();
538
- console.log(JSON.stringify(data, null, 2));
539
- } else if (action === "start") {
540
- const proxyUrl = args[1];
541
- if (!proxyUrl) {
542
- console.error(" Error: 'proxy start' requires a proxy URL, e.g.: iframer proxy start socks5://host:port");
543
- console.error(" To route through your home IP automatically, use the iframer MCP tool.");
705
+ console.log(JSON.stringify(readProxyState(), null, 2));
706
+ break;
707
+ }
708
+
709
+ if (action === "stop") {
710
+ const state = readProxyState();
711
+ if (!state.active) { console.log(JSON.stringify({ ok: true, message: "No proxy running" })); break; }
712
+ try { process.kill(state.pid, "SIGTERM"); } catch {}
713
+ await new Promise(r => setTimeout(r, 1500));
714
+ console.log(JSON.stringify({ ok: true, message: "Proxy stopped" }));
715
+ break;
716
+ }
717
+
718
+ if (action === "start") {
719
+ if (isDaemon) {
720
+ // Fork self with --foreground, detach, wait for proxy.json
721
+ const child = spawn(process.execPath, [__filename, "proxy", "start", "--foreground"], {
722
+ detached: true, stdio: "ignore", env: process.env,
723
+ });
724
+ child.unref();
725
+ const deadline = Date.now() + 20000;
726
+ while (Date.now() < deadline) {
727
+ await new Promise(r => setTimeout(r, 500));
728
+ const state = readProxyState();
729
+ if (state.active) { console.log(JSON.stringify({ ok: true, ...state })); process.exit(0); }
730
+ }
731
+ console.error(JSON.stringify({ ok: false, error: "Proxy daemon failed to start within 20s" }));
544
732
  process.exit(1);
545
733
  }
546
- const res = await fetch(`${getServer()}/proxy`, {
547
- method: "POST",
548
- headers: { "Content-Type": "application/json" },
549
- body: JSON.stringify({ server: proxyUrl }),
550
- });
551
- const data = await res.json();
552
- console.log(JSON.stringify(data, null, 2));
734
+
735
+ // Foreground mode (both --foreground and plain start)
736
+ await runProxyDaemon();
737
+ break;
553
738
  }
554
739
  break;
555
740
  }
package/mcp-server.cjs CHANGED
@@ -28388,172 +28388,43 @@ class StdioServerTransport {
28388
28388
 
28389
28389
  // src/mcp/server.ts
28390
28390
  import { readFileSync } from "fs";
28391
- import { join } from "path";
28391
+ import { join, dirname } from "path";
28392
+ import { fileURLToPath } from "url";
28392
28393
  import { homedir } from "os";
28393
-
28394
- // src/proxy/home-proxy.ts
28395
- import net from "net";
28396
- var state = { server: null, port: 0, active: false };
28397
- function handleSocks5(socket) {
28398
- socket.on("error", () => socket.destroy());
28399
- socket.once("data", (greeting) => {
28400
- if (greeting[0] !== 5) {
28401
- socket.destroy();
28402
- return;
28394
+ import { execFile } from "child_process";
28395
+ var BASE_URL = process.env.IFRAMER_URL || "https://api.iframer.sh";
28396
+ var CREDENTIALS_PATH = join(homedir(), ".iframer", "credentials.json");
28397
+ var PROXY_FILE = join(homedir(), ".iframer", "proxy.json");
28398
+ var _thisFile = fileURLToPath(import.meta.url);
28399
+ var CLI_PATH = _thisFile.endsWith(".cjs") ? join(dirname(_thisFile), "cli.js") : join(dirname(_thisFile), "../../bin/cli.js");
28400
+ function readProxyState() {
28401
+ try {
28402
+ const data = JSON.parse(readFileSync(PROXY_FILE, "utf8"));
28403
+ try {
28404
+ process.kill(data.pid, 0);
28405
+ return { active: true, ...data };
28406
+ } catch {
28407
+ return { active: false };
28403
28408
  }
28404
- socket.write(Buffer.from([5, 0]));
28405
- socket.once("data", (req) => {
28406
- if (req[0] !== 5 || req[1] !== 1) {
28407
- socket.destroy();
28409
+ } catch {
28410
+ return { active: false };
28411
+ }
28412
+ }
28413
+ function spawnCliProxy(cliArgs) {
28414
+ return new Promise((resolve) => {
28415
+ execFile(process.execPath, [CLI_PATH, ...cliArgs], { timeout: 25000 }, (err, stdout, stderr) => {
28416
+ if (err) {
28417
+ resolve({ ok: false, error: stderr || err.message });
28408
28418
  return;
28409
28419
  }
28410
- let host;
28411
- let port;
28412
- const addrType = req[3];
28413
- if (addrType === 1) {
28414
- host = `${req[4]}.${req[5]}.${req[6]}.${req[7]}`;
28415
- port = req.readUInt16BE(8);
28416
- } else if (addrType === 3) {
28417
- const len = req[4];
28418
- host = req.slice(5, 5 + len).toString();
28419
- port = req.readUInt16BE(5 + len);
28420
- } else if (addrType === 4) {
28421
- const parts = [];
28422
- for (let i = 0;i < 16; i += 2)
28423
- parts.push(req.readUInt16BE(4 + i).toString(16));
28424
- host = parts.join(":");
28425
- port = req.readUInt16BE(20);
28426
- } else {
28427
- socket.destroy();
28428
- return;
28420
+ try {
28421
+ resolve({ ok: true, ...JSON.parse(stdout) });
28422
+ } catch {
28423
+ resolve({ ok: false, error: stdout || "no output from proxy daemon" });
28429
28424
  }
28430
- const target = net.connect(port, host, () => {
28431
- const reply = Buffer.alloc(10);
28432
- reply[0] = 5;
28433
- reply[1] = 0;
28434
- reply[2] = 0;
28435
- reply[3] = 1;
28436
- socket.write(reply);
28437
- socket.pipe(target);
28438
- target.pipe(socket);
28439
- });
28440
- target.on("error", () => socket.destroy());
28441
- socket.on("close", () => target.destroy());
28442
- });
28443
- });
28444
- }
28445
- function startHomeProxy(preferredPort = 0) {
28446
- return new Promise((resolve, reject) => {
28447
- if (state.active && state.server) {
28448
- resolve(state.port);
28449
- return;
28450
- }
28451
- const server = net.createServer(handleSocks5);
28452
- server.listen(preferredPort, "0.0.0.0", () => {
28453
- state.server = server;
28454
- state.port = server.address().port;
28455
- state.active = true;
28456
- resolve(state.port);
28457
- });
28458
- server.on("error", reject);
28459
- });
28460
- }
28461
- function stopHomeProxy() {
28462
- return new Promise((resolve) => {
28463
- if (!state.server) {
28464
- resolve();
28465
- return;
28466
- }
28467
- state.server.close(() => {
28468
- state.server = null;
28469
- state.active = false;
28470
- state.port = 0;
28471
- resolve();
28472
28425
  });
28473
28426
  });
28474
28427
  }
28475
- function getHomeProxyInfo() {
28476
- return {
28477
- active: state.active,
28478
- port: state.port,
28479
- proxyUrl: state.active ? `socks5://host.docker.internal:${state.port}` : null
28480
- };
28481
- }
28482
-
28483
- // src/proxy/tunnel-client.ts
28484
- import net2 from "net";
28485
- async function startTunnel(apiBaseUrl, authToken) {
28486
- const localSocksPort = await startHomeProxy();
28487
- const wsUrl = apiBaseUrl.replace(/^http/, "ws") + "/proxy/tunnel";
28488
- const ws = new globalThis.WebSocket(wsUrl);
28489
- return new Promise((resolve, reject) => {
28490
- const sockets = new Map;
28491
- let done = false;
28492
- const fail = (reason) => {
28493
- if (!done) {
28494
- done = true;
28495
- reject(new Error(reason));
28496
- }
28497
- ws.close();
28498
- };
28499
- const stop = () => {
28500
- ws.close();
28501
- sockets.forEach((s) => s.destroy());
28502
- sockets.clear();
28503
- };
28504
- ws.onopen = () => {
28505
- ws.send(JSON.stringify({ type: "auth", token: authToken }));
28506
- };
28507
- ws.onmessage = (event) => {
28508
- try {
28509
- const msg = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString());
28510
- if (msg.type === "ready") {
28511
- if (!done) {
28512
- done = true;
28513
- resolve({ remotePort: msg.port, stop });
28514
- }
28515
- return;
28516
- }
28517
- if (msg.type === "open") {
28518
- const socket = net2.connect(localSocksPort, "127.0.0.1");
28519
- sockets.set(msg.id, socket);
28520
- socket.on("data", (data) => {
28521
- if (ws.readyState === 1) {
28522
- ws.send(JSON.stringify({ type: "data", id: msg.id, data: data.toString("base64") }));
28523
- }
28524
- });
28525
- socket.on("close", () => {
28526
- sockets.delete(msg.id);
28527
- if (ws.readyState === 1)
28528
- ws.send(JSON.stringify({ type: "close", id: msg.id }));
28529
- });
28530
- socket.on("error", () => socket.destroy());
28531
- return;
28532
- }
28533
- if (msg.type === "data") {
28534
- sockets.get(msg.id)?.write(Buffer.from(msg.data, "base64"));
28535
- return;
28536
- }
28537
- if (msg.type === "close") {
28538
- sockets.get(msg.id)?.destroy();
28539
- sockets.delete(msg.id);
28540
- }
28541
- } catch {}
28542
- };
28543
- ws.onerror = (err) => fail(`WebSocket error: ${err?.message ?? "unknown"}`);
28544
- ws.onclose = (event) => {
28545
- sockets.forEach((s) => s.destroy());
28546
- sockets.clear();
28547
- fail(`Tunnel closed by server (code=${event?.code ?? "?"}, reason=${event?.reason ?? "unknown"})`);
28548
- };
28549
- setTimeout(() => fail("Tunnel setup timed out (10s)"), 1e4);
28550
- });
28551
- }
28552
-
28553
- // src/mcp/server.ts
28554
- var activeTunnel = null;
28555
- var BASE_URL = process.env.IFRAMER_URL || "https://api.iframer.sh";
28556
- var CREDENTIALS_PATH = join(homedir(), ".iframer", "credentials.json");
28557
28428
  var cachedToken = null;
28558
28429
  function loadToken() {
28559
28430
  if (cachedToken)
@@ -28884,49 +28755,41 @@ WHEN TO USE: Try this whenever a site shows a captcha or bot detection wall. The
28884
28755
  }, async ({ action }) => {
28885
28756
  try {
28886
28757
  if (action === "status") {
28887
- const info = getHomeProxyInfo();
28758
+ const state = readProxyState();
28888
28759
  const apiProxy = await fetch(`${BASE_URL}/proxy`).then((r) => r.json()).catch(() => null);
28889
28760
  return { content: [{ type: "text", text: JSON.stringify({
28890
- homeProxy: info,
28891
- apiProxy: apiProxy?.proxy ?? "unknown",
28892
- apiProxySource: apiProxy?.source ?? "unknown"
28761
+ daemon: state,
28762
+ apiProxy: apiProxy?.proxy ?? null,
28763
+ apiProxySource: apiProxy?.source ?? null
28893
28764
  }, null, 2) }] };
28894
28765
  }
28895
28766
  if (action === "start") {
28896
- const isLocal = BASE_URL.includes("localhost") || BASE_URL.includes("127.0.0.1");
28897
- let proxyUrl;
28898
- if (isLocal) {
28899
- const port = await startHomeProxy();
28900
- proxyUrl = `socks5://host.docker.internal:${port}`;
28901
- } else {
28902
- if (activeTunnel)
28903
- activeTunnel.stop();
28904
- const token = loadToken();
28905
- const tunnel = await startTunnel(BASE_URL, token);
28906
- activeTunnel = tunnel;
28907
- proxyUrl = `socks5://127.0.0.1:${tunnel.remotePort}`;
28908
- }
28909
- const res = await fetch(`${BASE_URL}/proxy`, {
28910
- method: "POST",
28911
- headers: { "Content-Type": "application/json" },
28912
- body: JSON.stringify({ server: proxyUrl })
28913
- }).then((r) => r.json()).catch((e) => ({ ok: false, error: e.message }));
28914
- if (!res.ok)
28915
- return err(`Home proxy started but failed to configure API: ${res.error}`);
28767
+ const existing = readProxyState();
28768
+ if (existing.active && existing.proxyUrl) {
28769
+ await apiPost("/proxy", { server: existing.proxyUrl }).catch(() => {});
28770
+ await apiPost("/interactive/stop").catch(() => {});
28771
+ return { content: [{ type: "text", text: "Home proxy already running. Session restarted." }] };
28772
+ }
28773
+ const result = await spawnCliProxy(["proxy", "start", "--daemon"]);
28774
+ if (!result.ok)
28775
+ return err(`Failed to start proxy daemon: ${result.error}`);
28776
+ const state = readProxyState();
28777
+ if (!state.active || !state.proxyUrl)
28778
+ return err("Proxy daemon started but proxy.json not found");
28779
+ await apiPost("/proxy", { server: state.proxyUrl }).catch(() => {});
28916
28780
  await apiPost("/interactive/stop").catch(() => {});
28917
- return { content: [{ type: "text", text: `Home proxy active. Session restarted. Browser traffic now exits through your Mac's home IP.` }] };
28781
+ return { content: [{ type: "text", text: `Home proxy active. Session restarted. Browser traffic now exits through your home IP.` }] };
28918
28782
  }
28919
28783
  if (action === "stop") {
28920
- activeTunnel?.stop();
28921
- activeTunnel = null;
28922
- await stopHomeProxy();
28923
- await fetch(`${BASE_URL}/proxy`, {
28924
- method: "POST",
28925
- headers: { "Content-Type": "application/json" },
28926
- body: JSON.stringify({ server: null })
28927
- }).catch(() => {});
28928
- await apiPost("/interactive/stop").catch(() => {});
28929
- return { content: [{ type: "text", text: "Home proxy stopped. Session restarted. Browser traffic will use the default proxy (IPRoyal) or no proxy." }] };
28784
+ const state = readProxyState();
28785
+ if (state.active && state.pid) {
28786
+ try {
28787
+ process.kill(state.pid, "SIGTERM");
28788
+ } catch {}
28789
+ }
28790
+ await apiPost("/proxy", { server: null }).catch(() => {});
28791
+ apiPost("/interactive/stop").catch(() => {});
28792
+ return { content: [{ type: "text", text: "Home proxy stopped. Session will restart on next use." }] };
28930
28793
  }
28931
28794
  return err("Unknown action");
28932
28795
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iframer-cli",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "CLI for iframer — browser access for AI agents",
5
5
  "bin": {
6
6
  "iframer": "./cli.js"