iframer-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/bun.lock +195 -0
  2. package/cli.cjs +575 -0
  3. package/index.js +520 -0
  4. package/package.json +29 -0
package/bun.lock ADDED
@@ -0,0 +1,195 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "agentic-browser-mcp",
7
+ "dependencies": {
8
+ "@modelcontextprotocol/sdk": "^1.12.0",
9
+ },
10
+ },
11
+ },
12
+ "packages": {
13
+ "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
14
+
15
+ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.28.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw=="],
16
+
17
+ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
18
+
19
+ "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
20
+
21
+ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
22
+
23
+ "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
24
+
25
+ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
26
+
27
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
28
+
29
+ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
30
+
31
+ "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
32
+
33
+ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
34
+
35
+ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
36
+
37
+ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
38
+
39
+ "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
40
+
41
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
42
+
43
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
44
+
45
+ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
46
+
47
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
48
+
49
+ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
50
+
51
+ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
52
+
53
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
54
+
55
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
56
+
57
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
58
+
59
+ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
60
+
61
+ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
62
+
63
+ "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
64
+
65
+ "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
66
+
67
+ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
68
+
69
+ "express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
70
+
71
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
72
+
73
+ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
74
+
75
+ "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
76
+
77
+ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
78
+
79
+ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
80
+
81
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
82
+
83
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
84
+
85
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
86
+
87
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
88
+
89
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
90
+
91
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
92
+
93
+ "hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
94
+
95
+ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
96
+
97
+ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
98
+
99
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
100
+
101
+ "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
102
+
103
+ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
104
+
105
+ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
106
+
107
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
108
+
109
+ "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
110
+
111
+ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
112
+
113
+ "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
114
+
115
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
116
+
117
+ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
118
+
119
+ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
120
+
121
+ "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
122
+
123
+ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
124
+
125
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
126
+
127
+ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
128
+
129
+ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
130
+
131
+ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
132
+
133
+ "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
134
+
135
+ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
136
+
137
+ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
138
+
139
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
140
+
141
+ "path-to-regexp": ["path-to-regexp@8.4.0", "", {}, "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg=="],
142
+
143
+ "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
144
+
145
+ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
146
+
147
+ "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
148
+
149
+ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
150
+
151
+ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
152
+
153
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
154
+
155
+ "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
156
+
157
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
158
+
159
+ "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
160
+
161
+ "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
162
+
163
+ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
164
+
165
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
166
+
167
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
168
+
169
+ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
170
+
171
+ "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
172
+
173
+ "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
174
+
175
+ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
176
+
177
+ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
178
+
179
+ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
180
+
181
+ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
182
+
183
+ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
184
+
185
+ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
186
+
187
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
188
+
189
+ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
190
+
191
+ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
192
+
193
+ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
194
+ }
195
+ }
package/cli.cjs ADDED
@@ -0,0 +1,575 @@
1
+ #!/usr/bin/env node
2
+
3
+ const http = require("http");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { execSync } = require("child_process");
7
+
8
+ const readline = require("readline");
9
+
10
+ const CONFIG_DIR = path.join(require("os").homedir(), ".iframer");
11
+ const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
12
+ const DEFAULT_SERVER = "https://api.iframer.sh";
13
+
14
+ function getServer() {
15
+ return process.env.IFRAMER_URL || DEFAULT_SERVER;
16
+ }
17
+
18
+ function loadToken() {
19
+ try {
20
+ const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf8"));
21
+ return data.token || null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function saveToken(token) {
28
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
29
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify({ token, savedAt: new Date().toISOString() }, null, 2));
30
+ }
31
+
32
+ function clearToken() {
33
+ try { fs.unlinkSync(CREDENTIALS_FILE); } catch {}
34
+ }
35
+
36
+ function openBrowser(url) {
37
+ try {
38
+ if (process.platform === "darwin") execSync(`open "${url}"`);
39
+ else if (process.platform === "win32") execSync(`start "${url}"`);
40
+ else execSync(`xdg-open "${url}"`);
41
+ } catch {
42
+ console.log(` Open this URL in your browser:\n ${url}`);
43
+ }
44
+ }
45
+
46
+ function prompt(question) {
47
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
48
+ return new Promise((resolve) => {
49
+ rl.question(question, (answer) => {
50
+ rl.close();
51
+ resolve(answer.trim());
52
+ });
53
+ });
54
+ }
55
+
56
+ function promptHidden(question) {
57
+ return new Promise((resolve) => {
58
+ process.stdout.write(question);
59
+ const stdin = process.stdin;
60
+ stdin.setRawMode(true);
61
+ stdin.resume();
62
+ stdin.setEncoding("utf8");
63
+
64
+ let input = "";
65
+ const onData = (char) => {
66
+ if (char === "\n" || char === "\r" || char === "\u0004") {
67
+ stdin.setRawMode(false);
68
+ stdin.pause();
69
+ stdin.removeListener("data", onData);
70
+ process.stdout.write("\n");
71
+ resolve(input);
72
+ } else if (char === "\u0003") {
73
+ // ctrl+c
74
+ process.stdout.write("\n");
75
+ process.exit(0);
76
+ } else if (char === "\u007f" || char === "\b") {
77
+ // backspace
78
+ if (input.length > 0) {
79
+ input = input.slice(0, -1);
80
+ process.stdout.write("\b \b");
81
+ }
82
+ } else {
83
+ input += char;
84
+ process.stdout.write("*");
85
+ }
86
+ };
87
+ stdin.on("data", onData);
88
+ });
89
+ }
90
+
91
+ function requireToken() {
92
+ const token = loadToken();
93
+ if (!token) {
94
+ console.error(" Not logged in. Run: iframer login");
95
+ process.exit(1);
96
+ }
97
+ return token;
98
+ }
99
+
100
+ function authHeaders(token) {
101
+ return {
102
+ "Content-Type": "application/json",
103
+ Authorization: `Bearer ${token}`,
104
+ };
105
+ }
106
+
107
+ async function apiPost(endpoint, body, token) {
108
+ const res = await fetch(`${getServer()}${endpoint}`, {
109
+ method: "POST",
110
+ headers: authHeaders(token),
111
+ body: body ? JSON.stringify(body) : undefined,
112
+ });
113
+ return res.json();
114
+ }
115
+
116
+ async function apiGet(endpoint, token) {
117
+ const res = await fetch(`${getServer()}${endpoint}`, {
118
+ headers: authHeaders(token),
119
+ });
120
+ return res.json();
121
+ }
122
+
123
+ // Strip screenshot from response, save to file, print the rest as JSON
124
+ function handleResponse(data, screenshotPath) {
125
+ const { screenshot, tileScreenshots, ...rest } = data;
126
+ if (screenshot && screenshotPath) {
127
+ fs.writeFileSync(screenshotPath, Buffer.from(screenshot, "base64"));
128
+ rest._screenshotSaved = screenshotPath;
129
+ }
130
+ if (tileScreenshots && tileScreenshots.length > 0) {
131
+ const tileDir = "/tmp/browser-tiles";
132
+ fs.mkdirSync(tileDir, { recursive: true });
133
+ const tilePaths = [];
134
+ for (const tile of tileScreenshots) {
135
+ if (tile.screenshot) {
136
+ const tilePath = `${tileDir}/tile-${tile.index}.png`;
137
+ fs.writeFileSync(tilePath, Buffer.from(tile.screenshot, "base64"));
138
+ tilePaths.push(tilePath);
139
+ }
140
+ }
141
+ rest._tilesSaved = tilePaths;
142
+ }
143
+ console.log(JSON.stringify(rest, null, 2));
144
+ if (!data.ok) process.exit(1);
145
+ }
146
+
147
+ // ─── Commands ────────────────────────────────────────────────────────
148
+
149
+ async function login() {
150
+ if (loadToken()) {
151
+ console.log("Already logged in. Use 'logout' first to re-authenticate.");
152
+ return;
153
+ }
154
+
155
+ const server = getServer();
156
+
157
+ return new Promise((resolve, reject) => {
158
+ const localServer = http.createServer((req, res) => {
159
+ const url = new URL(req.url, "http://localhost");
160
+ if (url.pathname === "/callback") {
161
+ const token = url.searchParams.get("token");
162
+ if (token) {
163
+ saveToken(token);
164
+ res.writeHead(200, { "Content-Type": "text/html" });
165
+ res.end(`<!DOCTYPE html><html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#4ade80"><h1>Authorized! You can close this tab.</h1></body></html>`);
166
+ console.log("\n Authorized! Token saved to ~/.iframer/credentials.json");
167
+ localServer.close();
168
+ resolve();
169
+ } else {
170
+ res.writeHead(400, { "Content-Type": "text/plain" });
171
+ res.end("Missing token");
172
+ localServer.close();
173
+ reject(new Error("No token received"));
174
+ }
175
+ } else {
176
+ res.writeHead(404);
177
+ res.end();
178
+ }
179
+ });
180
+
181
+ localServer.listen(0, () => {
182
+ const port = localServer.address().port;
183
+ const callbackUrl = `http://localhost:${port}/callback`;
184
+ const authUrl = `${server}/auth/device?callback=${encodeURIComponent(callbackUrl)}`;
185
+ console.log("\n Opening browser for authorization...");
186
+ openBrowser(authUrl);
187
+ console.log(`\n If the browser didn't open, visit:\n ${authUrl}\n`);
188
+ console.log(" Waiting for authorization...");
189
+ });
190
+
191
+ setTimeout(() => {
192
+ localServer.close();
193
+ reject(new Error("Authorization timed out after 2 minutes"));
194
+ }, 120_000);
195
+ });
196
+ }
197
+
198
+ // ─── CLI entry point ─────────────────────────────────────────────────
199
+
200
+ const [,, command, ...args] = process.argv;
201
+
202
+ async function main() {
203
+ switch (command) {
204
+ case "login":
205
+ await login();
206
+ break;
207
+
208
+ case "logout":
209
+ clearToken();
210
+ console.log(" Logged out. Token removed.");
211
+ break;
212
+
213
+ case "status": {
214
+ const token = loadToken();
215
+ if (token) {
216
+ console.log(` Logged in`);
217
+ console.log(` Server: ${getServer()}`);
218
+ console.log(` Credentials: ${CREDENTIALS_FILE}`);
219
+ } else {
220
+ console.log(" Not logged in. Run 'login' to authenticate.");
221
+ }
222
+ break;
223
+ }
224
+
225
+ // ─── Credentials ───────────────────────────────────────────────
226
+
227
+ case "credentials": {
228
+ const token = requireToken();
229
+ const sub = args[0];
230
+
231
+ if (sub === "add") {
232
+ let domain = args[1];
233
+ const body = {};
234
+
235
+ // Check if flags were passed (non-interactive mode)
236
+ const hasFlags = args.some(a => a.startsWith("--"));
237
+
238
+ if (hasFlags && domain) {
239
+ // Non-interactive: parse flags
240
+ body.domain = domain;
241
+ for (let i = 2; i < args.length; i++) {
242
+ if (args[i] === "--username" && args[i + 1]) body.username = args[++i];
243
+ else if (args[i] === "--password" && args[i + 1]) body.password = args[++i];
244
+ else if (args[i] === "--totp-secret" && args[i + 1]) body.totp_secret = args[++i];
245
+ }
246
+ } else {
247
+ // Interactive: prompt for each field
248
+ console.log("");
249
+ if (!domain) {
250
+ domain = await prompt(" Domain (e.g. github.com): ");
251
+ if (!domain) { console.error(" Domain is required."); process.exit(1); }
252
+ }
253
+ body.domain = domain;
254
+
255
+ console.log(`\n Storing credentials for ${domain}\n`);
256
+ body.username = await prompt(" Username / email: ");
257
+ body.password = await promptHidden(" Password: ");
258
+
259
+ const totp = await prompt(" TOTP secret (press Enter to skip): ");
260
+ if (totp) body.totp_secret = totp;
261
+ }
262
+
263
+ if (!body.username && !body.password) {
264
+ console.error(" Must provide at least username or password.");
265
+ process.exit(1);
266
+ }
267
+
268
+ const data = await apiPost("/credentials", body, token);
269
+ if (!data.ok) { console.error(` Error: ${data.error}`); process.exit(1); }
270
+ console.log(`\n Credentials stored for ${domain}`);
271
+
272
+ } else if (sub === "list") {
273
+ const data = await apiGet("/credentials", token);
274
+ if (!data.ok) { console.error(` Error: ${data.error}`); process.exit(1); }
275
+ if (data.domains.length === 0) {
276
+ console.log(" No credentials stored.");
277
+ } else {
278
+ console.log(" Stored credentials:");
279
+ for (const d of data.domains) console.log(` - ${d}`);
280
+ }
281
+
282
+ } else if (sub === "remove") {
283
+ const domain = args[1];
284
+ if (!domain) { console.error(" Usage: iframer credentials remove <domain>"); process.exit(1); }
285
+ const res = await fetch(`${getServer()}/credentials/${encodeURIComponent(domain)}`, {
286
+ method: "DELETE",
287
+ headers: authHeaders(token),
288
+ });
289
+ const data = await res.json();
290
+ if (!data.ok) { console.error(` Error: ${data.error}`); process.exit(1); }
291
+ console.log(` Credentials for ${domain} removed.`);
292
+
293
+ } else {
294
+ console.error(" Usage: iframer credentials <add|list|remove>");
295
+ process.exit(1);
296
+ }
297
+ break;
298
+ }
299
+
300
+ // ─── Headless fetch ────────────────────────────────────────────
301
+
302
+ case "fetch": {
303
+ const token = requireToken();
304
+ const url = args[0];
305
+ if (!url) {
306
+ console.error(" Usage: iframer fetch <url> [--extract <js>] [--html] [--sessionless]");
307
+ process.exit(1);
308
+ }
309
+ const options = {};
310
+ for (let i = 1; i < args.length; i++) {
311
+ if (args[i] === "--extract" && args[i + 1]) options.extract = args[++i];
312
+ else if (args[i] === "--html") options.returnHtml = true;
313
+ else if (args[i] === "--sessionless") options.sessionless = true;
314
+ else if (args[i] === "--wait-for" && args[i + 1]) options.waitForSelector = args[++i];
315
+ else if (args[i] === "--browser" && args[i + 1]) options.browser = args[++i];
316
+ }
317
+ const data = await apiPost("/fetch", { url, ...options }, token);
318
+ console.log(JSON.stringify(data, null, 2));
319
+ if (!data.ok) process.exit(1);
320
+ break;
321
+ }
322
+
323
+ // ─── Interactive session management ────────────────────────────
324
+
325
+ case "interactive": {
326
+ const token = requireToken();
327
+ const sub = args[0];
328
+
329
+ if (sub === "stop") {
330
+ const data = await apiPost("/interactive/stop", null, token);
331
+ if (!data.ok) { console.error(` Error: ${data.error}`); process.exit(1); }
332
+ console.log(" Interactive session stopped. Session saved.");
333
+
334
+ } else if (sub === "status") {
335
+ const data = await apiGet("/interactive/status", token);
336
+ if (!data.ok) { console.error(` Error: ${data.error}`); process.exit(1); }
337
+ if (!data.active) {
338
+ console.log(" No active interactive session.");
339
+ } else {
340
+ console.log(` Active session`);
341
+ console.log(` noVNC: ${data.noVncUrl}`);
342
+ console.log(` Started: ${data.createdAt}`);
343
+ }
344
+
345
+ } else if (sub) {
346
+ // Start session with URL
347
+ const data = await apiPost("/interactive/start", { url: sub }, token);
348
+ if (!data.ok) { console.error(` Error: ${data.error}`); process.exit(1); }
349
+ console.log(`\n Interactive session started!`);
350
+ console.log(` noVNC: ${data.noVncUrl}\n`);
351
+ console.log(` Stop with: iframer interactive stop`);
352
+ openBrowser(data.noVncUrl);
353
+
354
+ } else {
355
+ console.error(" Usage: iframer interactive <url|stop|status>");
356
+ process.exit(1);
357
+ }
358
+ break;
359
+ }
360
+
361
+ // ─── Watch (poll for active session and open noVNC) ─────────────
362
+
363
+ case "watch": {
364
+ const token = requireToken();
365
+ console.log(" Watching for interactive session...\n");
366
+
367
+ const poll = async () => {
368
+ try {
369
+ const data = await apiGet("/interactive/status", token);
370
+ if (data.ok && data.active) return data.noVncUrl;
371
+ } catch {}
372
+ return null;
373
+ };
374
+
375
+ // Check immediately
376
+ let vncUrl = await poll();
377
+ if (vncUrl) {
378
+ console.log(` Session active! Opening noVNC viewer...`);
379
+ console.log(` ${vncUrl}\n`);
380
+ openBrowser(vncUrl);
381
+ }
382
+
383
+ // Keep polling and reopen if session restarts
384
+ let lastUrl = vncUrl;
385
+ const interval = setInterval(async () => {
386
+ const url = await poll();
387
+ if (url && url !== lastUrl) {
388
+ console.log(` New session detected! Opening noVNC viewer...`);
389
+ console.log(` ${url}\n`);
390
+ openBrowser(url);
391
+ }
392
+ lastUrl = url;
393
+ }, 2000);
394
+
395
+ // Keep process alive, clean exit on ctrl+c
396
+ process.on("SIGINT", () => {
397
+ clearInterval(interval);
398
+ console.log("\n Stopped watching.");
399
+ process.exit(0);
400
+ });
401
+
402
+ // Block forever
403
+ await new Promise(() => {});
404
+ break;
405
+ }
406
+
407
+ // ─── Screenshot ────────────────────────────────────────────────
408
+
409
+ case "screenshot": {
410
+ const token = requireToken();
411
+ const outPath = args[0] || "/tmp/browser-screenshot.png";
412
+ const res = await fetch(`${getServer()}/interactive/screenshot?format=raw`, {
413
+ headers: authHeaders(token),
414
+ });
415
+ if (!res.ok) {
416
+ const data = await res.json();
417
+ console.error(` Error: ${data.error}`);
418
+ process.exit(1);
419
+ }
420
+ const buffer = Buffer.from(await res.arrayBuffer());
421
+ fs.writeFileSync(outPath, buffer);
422
+ console.log(outPath);
423
+ break;
424
+ }
425
+
426
+ // ─── Act (send action to interactive session) ──────────────────
427
+
428
+ case "act": {
429
+ const token = requireToken();
430
+ const actionType = args[0];
431
+ if (!actionType) {
432
+ console.error(` Usage: iframer act <action-type> [options]
433
+
434
+ Actions:
435
+ click <selector> Click an element
436
+ human-click <selector> Click with human-like mouse movement
437
+ human-click <x> <y> Click at coordinates with human-like movement
438
+ human-type <selector> <text> Type with human-like keystroke timing
439
+ navigate <url> Navigate to a URL
440
+ scroll [deltaY] Scroll the page
441
+ wait <ms> Wait for milliseconds
442
+ evaluate <expression> Evaluate JavaScript
443
+ wait-for-selector <selector> Wait for element to appear
444
+ keyboard <key> Press a keyboard key
445
+
446
+ reCAPTCHA:
447
+ recaptcha-click Click the reCAPTCHA checkbox
448
+ recaptcha-select <tiles...> Click tiles by index (e.g. 0 2 5)
449
+ recaptcha-verify Click the verify button
450
+ recaptcha-info Get challenge info without clicking
451
+
452
+ Output:
453
+ Every action returns a screenshot saved to /tmp/browser-act.png`);
454
+ process.exit(1);
455
+ }
456
+
457
+ let action = {};
458
+ const screenshotPath = "/tmp/browser-act.png";
459
+
460
+ switch (actionType) {
461
+ case "click":
462
+ action = { type: "click", selector: args[1] };
463
+ break;
464
+ case "human-click":
465
+ if (args[1] && !isNaN(args[1]) && args[2] && !isNaN(args[2])) {
466
+ action = { type: "human-click", x: parseFloat(args[1]), y: parseFloat(args[2]) };
467
+ } else {
468
+ action = { type: "human-click", selector: args[1] };
469
+ }
470
+ break;
471
+ case "human-type":
472
+ action = { type: "human-type", selector: args[1], value: args.slice(2).join(" ") };
473
+ break;
474
+ case "navigate":
475
+ action = { type: "navigate", url: args[1], waitUntil: args[2] || "networkidle" };
476
+ break;
477
+ case "scroll":
478
+ action = { type: "scroll", deltaY: args[1] ? parseInt(args[1]) : undefined };
479
+ break;
480
+ case "wait":
481
+ action = { type: "wait", ms: parseInt(args[1]) || 1000 };
482
+ break;
483
+ case "evaluate":
484
+ action = { type: "evaluate", expression: args.slice(1).join(" ") };
485
+ break;
486
+ case "wait-for-selector":
487
+ action = { type: "wait-for-selector", selector: args[1], timeout: args[2] ? parseInt(args[2]) : undefined };
488
+ break;
489
+ case "keyboard":
490
+ action = { type: "keyboard", key: args[1] };
491
+ break;
492
+ case "recaptcha-click":
493
+ action = { type: "recaptcha-click" };
494
+ break;
495
+ case "recaptcha-select":
496
+ action = { type: "recaptcha-select", tiles: args.slice(1).map(Number) };
497
+ break;
498
+ case "recaptcha-verify":
499
+ action = { type: "recaptcha-verify" };
500
+ break;
501
+ case "recaptcha-info":
502
+ action = { type: "recaptcha-info" };
503
+ break;
504
+ default:
505
+ console.error(` Unknown action: ${actionType}`);
506
+ process.exit(1);
507
+ }
508
+
509
+ const data = await apiPost("/interactive/act", { action }, token);
510
+ handleResponse(data, screenshotPath);
511
+ break;
512
+ }
513
+
514
+ // ─── Help ──────────────────────────────────────────────────────
515
+
516
+ default:
517
+ console.log(`
518
+ iframer - CLI for the Agentic Browser API
519
+
520
+ Commands:
521
+ login Authenticate with the server
522
+ logout Remove saved credentials
523
+ status Show current auth status
524
+
525
+ Credentials:
526
+ credentials add <domain> Store login credentials (encrypted, server-side)
527
+ --username <user> Username or email
528
+ --password <pass> Password
529
+ --totp-secret <secret> TOTP secret for 2FA
530
+ credentials list List domains with stored credentials
531
+ credentials remove <domain> Delete credentials for a domain
532
+
533
+ Headless:
534
+ fetch <url> [options] Fetch a URL through the headless browser
535
+
536
+ Interactive (headful):
537
+ interactive <url> Open a live browser session
538
+ interactive stop Stop session and save state
539
+ interactive status Check if session is active
540
+ watch Watch the agent work (opens noVNC when session starts)
541
+ screenshot [path] Take a screenshot (default: /tmp/browser-screenshot.png)
542
+ act <action> [args...] Send an action to the interactive browser
543
+
544
+ Actions (use with 'act'):
545
+ click <selector> Click an element
546
+ human-click <selector|x y> Human-like click
547
+ human-type <selector> <text> Human-like typing
548
+ navigate <url> Go to URL
549
+ scroll [pixels] Scroll page
550
+ wait <ms> Wait
551
+ evaluate <js> Run JavaScript
552
+ keyboard <key> Press key
553
+ recaptcha-click Click reCAPTCHA checkbox
554
+ recaptcha-select <tiles...> Select tiles (e.g. act recaptcha-select 0 2 5)
555
+ recaptcha-verify Click verify button
556
+ recaptcha-info Get challenge metadata
557
+
558
+ Fetch options:
559
+ --extract <js> JavaScript to evaluate on page
560
+ --html Return full page HTML
561
+ --sessionless Skip session persistence
562
+ --wait-for <selector> Wait for element
563
+ --browser <name> Browser preference
564
+
565
+ Environment:
566
+ IFRAMER_URL Server URL (default: ${DEFAULT_SERVER})
567
+ `);
568
+ break;
569
+ }
570
+ }
571
+
572
+ main().catch((err) => {
573
+ console.error(` ${err.message}`);
574
+ process.exit(1);
575
+ });
package/index.js ADDED
@@ -0,0 +1,520 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { readFileSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+
9
+ // ─── Config ──────────────────────────────────────────────────────────
10
+
11
+ const BASE_URL = process.env.IFRAMER_URL || "https://api.iframer.sh";
12
+ const CREDENTIALS_PATH = join(homedir(), ".iframer", "credentials.json");
13
+
14
+ let cachedToken = null;
15
+
16
+ function loadToken() {
17
+ if (cachedToken) return cachedToken;
18
+ try {
19
+ const data = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf8"));
20
+ cachedToken = data.token;
21
+ return cachedToken;
22
+ } catch {
23
+ throw new Error("Not logged in. Run: iframer login");
24
+ }
25
+ }
26
+
27
+ function authHeaders() {
28
+ return {
29
+ "Content-Type": "application/json",
30
+ Authorization: `Bearer ${loadToken()}`,
31
+ };
32
+ }
33
+
34
+ async function apiPost(endpoint, body) {
35
+ const res = await fetch(`${BASE_URL}${endpoint}`, {
36
+ method: "POST",
37
+ headers: authHeaders(),
38
+ body: body ? JSON.stringify(body) : undefined,
39
+ });
40
+ return res.json();
41
+ }
42
+
43
+ async function apiGet(endpoint) {
44
+ const res = await fetch(`${BASE_URL}${endpoint}`, { headers: authHeaders() });
45
+ return res.json();
46
+ }
47
+
48
+ async function apiDelete(endpoint) {
49
+ const res = await fetch(`${BASE_URL}${endpoint}`, {
50
+ method: "DELETE",
51
+ headers: authHeaders(),
52
+ });
53
+ return res.json();
54
+ }
55
+
56
+ function errorResponse(message) {
57
+ return { content: [{ type: "text", text: message }], isError: true };
58
+ }
59
+
60
+ // ─── MCP Server ──────────────────────────────────────────────────────
61
+
62
+ const server = new McpServer({
63
+ name: "iframer",
64
+ version: "1.0.0",
65
+ instructions: `iframer — a headful/headless browser for AI agents.
66
+
67
+ CRITICAL: NEVER use the CLI (bin/cli.js) or bash commands. NEVER tell the user to run CLI commands. Use ONLY these MCP tools. Everything works through MCP — no bash, no curl, no CLI.
68
+
69
+ WORKFLOW:
70
+ 1. Call "status" FIRST to check API, auth, sessions, and credentials.
71
+ 2. For simple page fetches, use "browse" (headless, fast, cheap).
72
+ 3. For anything requiring vision (CAPTCHAs, login forms, 2FA), use "interactive_start" then "interactive_act".
73
+ 4. Screenshots are returned as URLs. Use Read tool to view them ONLY when you need to see the page.
74
+ 5. Skip screenshots (screenshot: false) when you already know what to click.
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
+ 7. Always call "interactive_stop" when done to save session state.
77
+
78
+ MINIMIZE TOKEN USAGE: Don't screenshot every action. Only look when you need to decide what to do next.
79
+ NEVER use bash/CLI. NEVER suggest CLI commands to the user. ALL interaction goes through these MCP tools.`,
80
+ });
81
+
82
+ // ─── Tool 0: status (call this first) ────────────────────────────────
83
+
84
+ server.tool(
85
+ "status",
86
+ `Get the full state of iframer in one call. CALL THIS FIRST before doing anything else. Returns: API health, auth status, active sessions, stored credentials. This eliminates the need for discovery.`,
87
+ {},
88
+ async () => {
89
+ try {
90
+ const status = { api: false, authenticated: false, session: null, credentials: [] };
91
+
92
+ // Check API health
93
+ try {
94
+ const health = await fetch(`${BASE_URL}/health`);
95
+ const data = await health.json();
96
+ status.api = data.ok === true;
97
+ } catch {
98
+ return { content: [{ type: "text", text: `API not running at ${BASE_URL}. Start it with: bun run start:docker` }], isError: true };
99
+ }
100
+
101
+ // Check auth
102
+ try {
103
+ loadToken();
104
+ status.authenticated = true;
105
+ } catch {
106
+ return { content: [{ type: "text", text: JSON.stringify({ ...status, authenticated: false, message: "Not logged in. Run: iframer login" }, null, 2) }] };
107
+ }
108
+
109
+ // Check interactive session
110
+ try {
111
+ const sessionData = await apiGet("/interactive/status");
112
+ if (sessionData.ok && sessionData.active) {
113
+ status.session = { active: true, noVncUrl: sessionData.noVncUrl, createdAt: sessionData.createdAt };
114
+ } else {
115
+ status.session = { active: false };
116
+ }
117
+ } catch {}
118
+
119
+ // Check stored credentials
120
+ try {
121
+ const credData = await apiGet("/credentials");
122
+ if (credData.ok) status.credentials = credData.domains;
123
+ } catch {}
124
+
125
+ return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
126
+ } catch (err) {
127
+ return errorResponse(`Error: ${err.message}`);
128
+ }
129
+ }
130
+ );
131
+
132
+ // ─── Tool 1: browse ──────────────────────────────────────────────────
133
+
134
+ server.tool(
135
+ "browse",
136
+ "Fetch a web page using a headless browser. Renders JavaScript, can execute actions (click, fill, scroll), and extract data. Session cookies persist across calls.",
137
+ {
138
+ url: z.string().describe("URL to navigate to"),
139
+ extract: z.string().optional().describe("JavaScript expression to evaluate on the page (e.g. 'document.title')"),
140
+ actions: z.array(z.object({
141
+ type: z.enum(["click", "fill", "wait", "scroll", "human-click", "human-type"]),
142
+ selector: z.string().optional(),
143
+ value: z.string().optional(),
144
+ ms: z.number().optional(),
145
+ })).optional().describe("Actions to execute before extracting"),
146
+ returnHtml: z.boolean().optional().describe("Return full page HTML"),
147
+ waitForSelector: z.string().optional().describe("Wait for this CSS selector before proceeding"),
148
+ sessionless: z.boolean().optional().describe("Skip session persistence for this request"),
149
+ },
150
+ async (params) => {
151
+ try {
152
+ const data = await apiPost("/fetch", params);
153
+ if (!data.ok) return errorResponse(`Error: ${data.error}`);
154
+ const { html, ...rest } = data;
155
+ const text = html
156
+ ? JSON.stringify(rest, null, 2) + "\n\n--- HTML ---\n" + html
157
+ : JSON.stringify(rest, null, 2);
158
+ return { content: [{ type: "text", text }] };
159
+ } catch (err) {
160
+ return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
161
+ }
162
+ }
163
+ );
164
+
165
+ // ─── Tool 2: interactive_start ───────────────────────────────────────
166
+
167
+ server.tool(
168
+ "interactive_start",
169
+ "Start an interactive headful browser session. Opens a real Chromium window (streamed via noVNC). Use this when you need to see and interact with the page — CAPTCHAs, 2FA, login flows, etc.",
170
+ {
171
+ url: z.string().optional().describe("URL to navigate to on start"),
172
+ },
173
+ async (params) => {
174
+ try {
175
+ const data = await apiPost("/interactive/start", params);
176
+ if (!data.ok) return errorResponse(`Error: ${data.error}`);
177
+ return {
178
+ content: [{
179
+ type: "text",
180
+ text: `Interactive session started.\nnoVNC viewer: ${data.noVncUrl}\n\nUse interactive_screenshot to see the page, interactive_act to interact with it.`,
181
+ }],
182
+ };
183
+ } catch (err) {
184
+ return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
185
+ }
186
+ }
187
+ );
188
+
189
+ // ─── Tool 3: interactive_screenshot ──────────────────────────────────
190
+
191
+ server.tool(
192
+ "interactive_screenshot",
193
+ "Take a screenshot of the current interactive browser session. Returns a URL to the image — use Read tool or WebFetch to view it.",
194
+ {},
195
+ async () => {
196
+ try {
197
+ const data = await apiGet("/interactive/screenshot");
198
+ if (!data.ok) return errorResponse(`Error: ${data.error}`);
199
+ return {
200
+ content: [{
201
+ type: "text",
202
+ text: `Page: ${data.title}\nURL: ${data.url}\nScreenshot: ${data.screenshotUrl}`,
203
+ }],
204
+ };
205
+ } catch (err) {
206
+ return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
207
+ }
208
+ }
209
+ );
210
+
211
+ // ─── Tool 4: interactive_act ─────────────────────────────────────────
212
+
213
+ server.tool(
214
+ "interactive_act",
215
+ `Send an action to the interactive browser and get a screenshot URL of the result.
216
+
217
+ Actions: click, human-click, human-type, fill, navigate, scroll, wait, evaluate, wait-for-selector, keyboard.
218
+ reCAPTCHA: recaptcha-click (click checkbox), recaptcha-select (select tiles by index), recaptcha-verify (click verify), recaptcha-info (get challenge metadata).
219
+
220
+ Screenshots are saved as files and returned as URLs. Use Read tool to view them. For reCAPTCHA, individual tile image URLs are returned.`,
221
+ {
222
+ action: z.enum([
223
+ "click", "human-click", "human-type", "fill",
224
+ "navigate", "scroll", "wait", "evaluate",
225
+ "wait-for-selector", "keyboard",
226
+ "recaptcha-click", "recaptcha-select", "recaptcha-verify", "recaptcha-info",
227
+ ]).describe("Action type to perform"),
228
+ selector: z.string().optional().describe("CSS selector (for click, fill, human-click, human-type, wait-for-selector)"),
229
+ value: z.string().optional().describe("Value to type (for fill, human-type)"),
230
+ url: z.string().optional().describe("URL for navigate action"),
231
+ x: z.number().optional().describe("X coordinate for human-click"),
232
+ y: z.number().optional().describe("Y coordinate for human-click"),
233
+ deltaY: z.number().optional().describe("Scroll pixels (default: full page)"),
234
+ ms: z.number().optional().describe("Milliseconds for wait action"),
235
+ expression: z.string().optional().describe("JavaScript for evaluate action"),
236
+ key: z.string().optional().describe("Key for keyboard action (e.g. Enter, Tab)"),
237
+ tiles: z.array(z.number()).optional().describe("Tile indices for recaptcha-select (e.g. [0, 2, 5])"),
238
+ timeout: z.number().optional().describe("Timeout in ms for wait-for-selector"),
239
+ screenshot: z.boolean().optional().describe("Set to false to skip screenshot (faster)"),
240
+ },
241
+ async (params) => {
242
+ try {
243
+ const { action: actionType, screenshot: wantScreenshot, ...rest } = params;
244
+ const actionObj = { type: actionType };
245
+
246
+ for (const [key, val] of Object.entries(rest)) {
247
+ if (val !== undefined) actionObj[key] = val;
248
+ }
249
+
250
+ const data = await apiPost("/interactive/act", { action: actionObj, screenshot: wantScreenshot });
251
+ if (!data.ok) return errorResponse(`Error: ${data.error}`);
252
+
253
+ const textData = { url: data.url, title: data.title };
254
+ if (data.result !== null && data.result !== undefined) textData.result = data.result;
255
+ if (data.screenshotUrl) textData.screenshotUrl = data.screenshotUrl;
256
+
257
+ // Include tile URLs for reCAPTCHA
258
+ if (data.tileUrls && data.tileUrls.length > 0) {
259
+ textData.tileUrls = data.tileUrls;
260
+ }
261
+
262
+ return { content: [{ type: "text", text: JSON.stringify(textData, null, 2) }] };
263
+ } catch (err) {
264
+ return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
265
+ }
266
+ }
267
+ );
268
+
269
+ // ─── Tool 4b: interactive_batch ──────────────────────────────────────
270
+
271
+ server.tool(
272
+ "interactive_batch",
273
+ `Run multiple actions in a single call. Much faster than calling interactive_act repeatedly.
274
+ Each action is an object with a "type" and its parameters. Actions run sequentially. Stops on first error unless continueOnError is true.
275
+ Only one screenshot is taken at the end (or none if screenshot: false).
276
+
277
+ Example: [
278
+ {"type": "navigate", "url": "https://example.com"},
279
+ {"type": "wait", "ms": 2000},
280
+ {"type": "click", "selector": "#login"},
281
+ {"type": "evaluate", "expression": "document.title"}
282
+ ]`,
283
+ {
284
+ actions: z.array(z.object({
285
+ type: z.string().describe("Action type"),
286
+ selector: z.string().optional(),
287
+ value: z.string().optional(),
288
+ url: z.string().optional(),
289
+ x: z.number().optional(),
290
+ y: z.number().optional(),
291
+ deltaY: z.number().optional(),
292
+ ms: z.number().optional(),
293
+ expression: z.string().optional(),
294
+ key: z.string().optional(),
295
+ tiles: z.array(z.number()).optional(),
296
+ timeout: z.number().optional(),
297
+ waitUntil: z.string().optional(),
298
+ })).describe("Array of actions to execute sequentially"),
299
+ screenshot: z.boolean().optional().describe("Take screenshot at the end (default true)"),
300
+ continueOnError: z.boolean().optional().describe("Continue executing actions even if one fails"),
301
+ },
302
+ async (params) => {
303
+ try {
304
+ const data = await apiPost("/interactive/batch", params);
305
+ if (!data.ok) return errorResponse(`Error: ${data.error}`);
306
+
307
+ const textData = {
308
+ url: data.url,
309
+ title: data.title,
310
+ results: data.results,
311
+ };
312
+ if (data.screenshotUrl) textData.screenshotUrl = data.screenshotUrl;
313
+
314
+ return { content: [{ type: "text", text: JSON.stringify(textData, null, 2) }] };
315
+ } catch (err) {
316
+ return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
317
+ }
318
+ }
319
+ );
320
+
321
+ // ─── Tool 5: interactive_stop ────────────────────────────────────────
322
+
323
+ server.tool(
324
+ "interactive_stop",
325
+ "Stop the interactive browser session and save session state (cookies, localStorage) for future use.",
326
+ {},
327
+ async () => {
328
+ try {
329
+ const data = await apiPost("/interactive/stop");
330
+ if (!data.ok) return errorResponse(`Error: ${data.error}`);
331
+ return {
332
+ content: [{ type: "text", text: `Session stopped. State saved: ${data.sessionSaved}` }],
333
+ };
334
+ } catch (err) {
335
+ return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
336
+ }
337
+ }
338
+ );
339
+
340
+ // ─── Tool 6: interactive_status ──────────────────────────────────────
341
+
342
+ server.tool(
343
+ "interactive_status",
344
+ "Check if an interactive browser session is currently active.",
345
+ {},
346
+ async () => {
347
+ try {
348
+ const data = await apiGet("/interactive/status");
349
+ if (!data.ok) return errorResponse(`Error: ${data.error}`);
350
+ if (!data.active) {
351
+ return { content: [{ type: "text", text: "No active interactive session." }] };
352
+ }
353
+ return {
354
+ content: [{
355
+ type: "text",
356
+ text: `Active session\nnoVNC: ${data.noVncUrl}\nStarted: ${data.createdAt}`,
357
+ }],
358
+ };
359
+ } catch (err) {
360
+ return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
361
+ }
362
+ }
363
+ );
364
+
365
+ // ─── Tool 7: clear_session ───────────────────────────────────────────
366
+
367
+ server.tool(
368
+ "clear_session",
369
+ "Clear all stored browser session data (cookies, localStorage). Start fresh on next browse.",
370
+ {},
371
+ async () => {
372
+ try {
373
+ const data = await apiDelete("/session");
374
+ if (!data.ok) return errorResponse(`Error: ${data.error}`);
375
+ return { content: [{ type: "text", text: "Session cleared." }] };
376
+ } catch (err) {
377
+ return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
378
+ }
379
+ }
380
+ );
381
+
382
+ // ─── Tool 8: login ──────────────────────────────────────────────────
383
+
384
+ server.tool(
385
+ "login",
386
+ `Log into a website using stored credentials. The server handles the login — passwords never enter the AI context.
387
+
388
+ Users must store credentials first via store_credentials tool or CLI.
389
+
390
+ The agent provides CSS selectors for the login form fields. The server decrypts credentials internally, fills the form with human-like typing, and clicks submit. If TOTP 2FA is configured, it generates and enters the code automatically.
391
+
392
+ IMPORTANT: You will never see the actual credentials. You only need to identify the form selectors on the page.`,
393
+ {
394
+ domain: z.string().describe("Domain to log into (must match stored credentials, e.g. 'github.com')"),
395
+ usernameSelector: z.string().optional().describe("CSS selector for the username/email input field"),
396
+ passwordSelector: z.string().optional().describe("CSS selector for the password input field"),
397
+ submitSelector: z.string().optional().describe("CSS selector for the submit/login button"),
398
+ totpSelector: z.string().optional().describe("CSS selector for the TOTP/2FA code input (if applicable)"),
399
+ },
400
+ async (params) => {
401
+ try {
402
+ const data = await apiPost("/credentials/login", params);
403
+ if (!data.ok) return errorResponse(`Error: ${data.error}`);
404
+
405
+ const result = {
406
+ message: data.message,
407
+ totpGenerated: data.totpGenerated,
408
+ url: data.url,
409
+ title: data.title,
410
+ };
411
+ if (data.screenshotUrl) result.screenshotUrl = data.screenshotUrl;
412
+
413
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
414
+ } catch (err) {
415
+ return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
416
+ }
417
+ }
418
+ );
419
+
420
+ // ─── Tool 9: list_credentials ───────────────────────────────────────
421
+
422
+ server.tool(
423
+ "list_credentials",
424
+ "List domains for which the user has stored login credentials. Returns only domain names, never the actual credential values.",
425
+ {},
426
+ async () => {
427
+ try {
428
+ const data = await apiGet("/credentials");
429
+ if (!data.ok) return errorResponse(`Error: ${data.error}`);
430
+ if (data.domains.length === 0) {
431
+ return { content: [{ type: "text", text: "No credentials stored. User can add them with: iframer credentials add <domain>" }] };
432
+ }
433
+ return {
434
+ content: [{ type: "text", text: `Stored credentials for:\n${data.domains.map(d => ` - ${d}`).join("\n")}` }],
435
+ };
436
+ } catch (err) {
437
+ return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
438
+ }
439
+ }
440
+ );
441
+
442
+ // ─── Tool 10: store_credentials ─────────────────────────────────────
443
+
444
+ server.tool(
445
+ "store_credentials",
446
+ `Store login credentials for a website. Prompts the user directly for their username, password, and optional TOTP secret. Credentials are encrypted and stored server-side — the AI never sees them.
447
+
448
+ ALWAYS call this tool when credentials are needed but not stored. NEVER tell the user to run CLI commands — this tool handles it via a secure prompt.`,
449
+ {
450
+ domain: z.string().describe("Domain to store credentials for (e.g. 'github.com', 'mercadolivre.com.br')"),
451
+ },
452
+ async ({ domain }) => {
453
+ try {
454
+ const result = await server.server.elicitInput({
455
+ mode: "form",
456
+ message: `Enter your login credentials for ${domain}.\nThese will be encrypted and stored securely. The AI will never see them.`,
457
+ requestedSchema: {
458
+ type: "object",
459
+ properties: {
460
+ username: {
461
+ type: "string",
462
+ title: "Username / Email",
463
+ description: "Your username or email for this site",
464
+ minLength: 1,
465
+ },
466
+ password: {
467
+ type: "string",
468
+ title: "Password",
469
+ description: "Your password",
470
+ minLength: 1,
471
+ },
472
+ totp_secret: {
473
+ type: "string",
474
+ title: "TOTP Secret (optional)",
475
+ description: "Your authenticator app secret key for automatic 2FA. Leave empty if not using TOTP.",
476
+ },
477
+ },
478
+ required: ["username", "password"],
479
+ },
480
+ });
481
+
482
+ if (result.action === "decline" || !result.content) {
483
+ return { content: [{ type: "text", text: "Credential storage cancelled by user." }] };
484
+ }
485
+
486
+ const { username, password, totp_secret } = result.content;
487
+ const data = await apiPost("/credentials", {
488
+ domain,
489
+ username,
490
+ password,
491
+ totp_secret: totp_secret || undefined,
492
+ });
493
+
494
+ if (!data.ok) return errorResponse(`Error: ${data.error}`);
495
+
496
+ return {
497
+ content: [{
498
+ type: "text",
499
+ text: `Credentials stored securely for ${domain}. Use the login tool to log in.`,
500
+ }],
501
+ };
502
+ } catch (err) {
503
+ if (err.message?.includes("does not support")) {
504
+ return {
505
+ content: [{
506
+ type: "text",
507
+ text: `This client doesn't support secure input prompts. Please store credentials via the CLI instead:\n\niframer credentials add ${domain}`,
508
+ }],
509
+ isError: true,
510
+ };
511
+ }
512
+ return errorResponse(`Error: ${err.message}`);
513
+ }
514
+ }
515
+ );
516
+
517
+ // ─── Start ───────────────────────────────────────────────────────────
518
+
519
+ const transport = new StdioServerTransport();
520
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "iframer-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI and MCP server for iframer — a headful/headless browser for AI agents",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "iframer-mcp": "./index.js",
9
+ "iframer": "./cli.cjs"
10
+ },
11
+ "keywords": [
12
+ "mcp",
13
+ "browser",
14
+ "ai-agent",
15
+ "playwright",
16
+ "headless",
17
+ "automation",
18
+ "claude",
19
+ "model-context-protocol"
20
+ ],
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/EduardoFazolo/iframer-agentic-browser"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.12.0"
28
+ }
29
+ }