iframer-toolkit 0.9.0 → 0.10.5

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.
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ // iframer native-messaging host: hands the machine-local pairing token to the
3
+ // iframer extension so nobody has to copy-paste it. Chrome launches this
4
+ // process (per its host manifest, installed by `iframer install extension`),
5
+ // restricted by allowed_origins to the iframer extension ID only.
6
+ //
7
+ // Protocol (Chrome native messaging): each message is a 4-byte little-endian
8
+ // length followed by that many bytes of JSON, both directions.
9
+ //
10
+ // This file is COPIED to ~/.iframer/ at install time so it keeps working if
11
+ // the repo moves. It must stay dependency-free (plain node, no imports beyond
12
+ // stdlib) and must read the same secret locations as src/lib/auth/crypto.ts.
13
+
14
+ const fs = require("fs");
15
+ const os = require("os");
16
+ const path = require("path");
17
+
18
+ function readToken() {
19
+ if (process.env.IFRAMER_SECRET) return process.env.IFRAMER_SECRET;
20
+ const dataDir = process.env.IFRAMER_DATA_DIR || path.join(os.homedir(), ".iframer");
21
+ const candidates = [
22
+ path.join(dataDir, "secret"),
23
+ path.join(process.env.XDG_RUNTIME_DIR || os.tmpdir(), "iframer-secret"),
24
+ ];
25
+ for (const file of candidates) {
26
+ try {
27
+ const t = fs.readFileSync(file, "utf8").trim();
28
+ if (t) return t;
29
+ } catch {
30
+ /* try next */
31
+ }
32
+ }
33
+ return "";
34
+ }
35
+
36
+ // Where is the iframer server right now? Reads the server registry the local
37
+ // server writes on startup (src/lib/browser/registry.ts) and checks the pid is
38
+ // actually alive — so the extension can connect straight to the right port
39
+ // instead of blind-scanning 21 ports (each dead dial logs a console error).
40
+ function readServerInfo() {
41
+ const dataDir = process.env.IFRAMER_DATA_DIR || path.join(os.homedir(), ".iframer");
42
+ try {
43
+ const info = JSON.parse(fs.readFileSync(path.join(dataDir, "server.json"), "utf8"));
44
+ let alive = false;
45
+ if (info && typeof info.pid === "number") {
46
+ try {
47
+ process.kill(info.pid, 0);
48
+ alive = true;
49
+ } catch {
50
+ /* pid gone — stale registry */
51
+ }
52
+ }
53
+ return { port: info && typeof info.port === "number" ? info.port : null, alive };
54
+ } catch {
55
+ return { port: null, alive: false };
56
+ }
57
+ }
58
+
59
+ function send(msg) {
60
+ const payload = Buffer.from(JSON.stringify(msg), "utf8");
61
+ const header = Buffer.alloc(4);
62
+ header.writeUInt32LE(payload.length, 0);
63
+ process.stdout.write(Buffer.concat([header, payload]));
64
+ }
65
+
66
+ let buf = Buffer.alloc(0);
67
+ process.stdin.on("data", (chunk) => {
68
+ buf = Buffer.concat([buf, chunk]);
69
+ while (buf.length >= 4) {
70
+ const len = buf.readUInt32LE(0);
71
+ if (buf.length < 4 + len) return;
72
+ const body = buf.subarray(4, 4 + len);
73
+ buf = buf.subarray(4 + len);
74
+ let msg = {};
75
+ try {
76
+ msg = JSON.parse(body.toString("utf8"));
77
+ } catch {
78
+ /* respond with error below */
79
+ }
80
+ if (msg && msg.cmd === "get-token") {
81
+ const token = readToken();
82
+ send(token ? { ok: true, token } : { ok: false, error: "no secret file found — run any iframer command once to create it" });
83
+ } else if (msg && msg.cmd === "get-info") {
84
+ // Token + live server location in one call.
85
+ send({ ok: true, token: readToken() || null, ...readServerInfo() });
86
+ } else {
87
+ send({ ok: false, error: "unknown command" });
88
+ }
89
+ }
90
+ });
91
+ process.stdin.on("end", () => process.exit(0));
@@ -0,0 +1,72 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <style>
6
+ body { font: 13px/1.4 -apple-system, system-ui, sans-serif; width: 300px; margin: 0; padding: 14px; color: #1a1a1a; }
7
+ h1 { font-size: 14px; margin: 0 0 10px; display: flex; align-items: center; gap: 6px; }
8
+ .dot { width: 9px; height: 9px; border-radius: 50%; background: #c33; display: inline-block; }
9
+ .dot.on { background: #2a2; }
10
+ .row { margin: 8px 0; }
11
+ label { display: block; font-weight: 600; margin-bottom: 3px; font-size: 12px; }
12
+ input { width: 100%; box-sizing: border-box; padding: 6px; border: 1px solid #ccc; border-radius: 5px; font-family: monospace; font-size: 11px; }
13
+ button { padding: 7px 10px; border: 0; border-radius: 6px; background: #1a1a1a; color: #fff; cursor: pointer; font-size: 12px; }
14
+ button.secondary { background: #eee; color: #1a1a1a; }
15
+ button:disabled { opacity: 0.5; cursor: default; }
16
+ .muted { color: #777; font-size: 11px; }
17
+ .tab { font-size: 11px; color: #555; word-break: break-all; margin: 4px 0; }
18
+ .status { font-size: 12px; margin-bottom: 8px; }
19
+ .actions { display: flex; gap: 6px; }
20
+ hr { border: 0; border-top: 1px solid #eee; margin: 12px 0; }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <h1><span id="dot" class="dot"></span> iframer</h1>
25
+ <div class="status" id="status">Checking…</div>
26
+ <div id="update" style="display:none; margin:6px 0 10px; padding:8px 10px; background:#e8f7ef; border:1px solid #b6e6cd; border-radius:6px; font-size:12px; color:#1a5e3a;"></div>
27
+
28
+ <div class="row">
29
+ <label>Profile</label>
30
+ <div class="muted" id="profinfo" style="margin-bottom:4px;">detecting…</div>
31
+ <input id="label" type="text" placeholder="optional: rename this profile" />
32
+ <div class="actions" style="margin-top:6px;">
33
+ <button id="savelabel" class="secondary">Override name</button>
34
+ </div>
35
+ </div>
36
+
37
+ <hr />
38
+
39
+ <div class="row">
40
+ <label>Pairing</label>
41
+ <div class="actions">
42
+ <button id="autopair">Pair automatically</button>
43
+ </div>
44
+ <div class="muted" id="pairmsg" style="margin-top:4px;">Needs <code>iframer install extension</code> run once in a terminal. Or paste the token below.</div>
45
+ </div>
46
+
47
+ <div class="row">
48
+ <label for="token">Pairing token (manual fallback)</label>
49
+ <input id="token" type="password" placeholder="paste from ~/.iframer/secret" />
50
+ </div>
51
+ <div class="actions">
52
+ <button id="save" class="secondary">Save &amp; connect</button>
53
+ </div>
54
+
55
+ <div class="muted" id="tabnote" style="margin-top:10px;">
56
+ When connected, iframer can find and drive any of your open tabs.
57
+ </div>
58
+
59
+ <hr />
60
+
61
+ <div class="row">
62
+ <label>Page access</label>
63
+ <div class="actions">
64
+ <button id="grant" class="secondary">Grant all-sites access</button>
65
+ <button id="test" class="secondary">Test page access</button>
66
+ </div>
67
+ <div class="muted" id="diag" style="margin-top:6px; white-space:pre-wrap;"></div>
68
+ </div>
69
+
70
+ <script src="popup.js"></script>
71
+ </body>
72
+ </html>
@@ -0,0 +1,108 @@
1
+ const $ = (id) => document.getElementById(id);
2
+
3
+ function send(cmd, extra = {}) {
4
+ // Resolve to {} when the worker is asleep or errored (sendMessage passes
5
+ // undefined + sets lastError) so callers never null-deref on res.status etc.
6
+ return new Promise((resolve) =>
7
+ chrome.runtime.sendMessage({ cmd, ...extra }, (resp) => {
8
+ void chrome.runtime.lastError; // touch it so Chrome doesn't log "Unchecked"
9
+ resolve(resp || {});
10
+ }),
11
+ );
12
+ }
13
+
14
+ function renderStatus(status) {
15
+ const connected = status && status.connected;
16
+ $("dot").className = "dot" + (connected ? " on" : "");
17
+ const reasonText = {
18
+ "no-token": "No token set — try Pair automatically, or paste it below.",
19
+ "server-not-found": "iframer server not found. Is Claude Code / iframer running?",
20
+ "server-not-running": "iframer server not running — it starts with your Claude/iframer session. Will connect by itself.",
21
+ "bad-token": "Token rejected. Check it matches ~/.iframer/secret.",
22
+ disconnected: "Disconnected — retrying…",
23
+ };
24
+ if (connected) {
25
+ $("status").textContent = `Connected on port ${status.port}. iframer can drive any open tab.`;
26
+ } else {
27
+ $("status").textContent = reasonText[status && status.reason] || "Not connected.";
28
+ }
29
+ // Update-available notice (check-and-notify).
30
+ const up = $("update");
31
+ if (up) {
32
+ if (status && status.updateAvailable) {
33
+ up.style.display = "block";
34
+ up.textContent = `↑ Update available: v${status.updateAvailable} (you have v${status.current || "?"}). Run \`iframer update\` in a terminal.`;
35
+ } else {
36
+ up.style.display = "none";
37
+ }
38
+ }
39
+ }
40
+
41
+ async function refresh() {
42
+ const state = await send("get-state");
43
+ renderStatus(state.status);
44
+ }
45
+
46
+ async function init() {
47
+ const { token, profileLabel } = await chrome.storage.local.get(["token", "profileLabel"]);
48
+ if (token) $("token").value = token;
49
+ if (profileLabel) $("label").value = profileLabel;
50
+ const state = await send("get-state");
51
+ renderStatus(state.status);
52
+ if (state.profileName) $("profinfo").textContent = `${state.profileName} · id ${String(state.profileId).slice(0, 8)}`;
53
+ }
54
+
55
+ $("savelabel").addEventListener("click", async () => {
56
+ const res = await send("set-label", { label: $("label").value });
57
+ if (res.profileName) $("profinfo").textContent = `${res.profileName} · id ${String(res.profileId).slice(0, 8)}`;
58
+ });
59
+
60
+ $("grant").addEventListener("click", () => {
61
+ // Must run in the popup (user gesture) — requesting host access to all sites.
62
+ chrome.permissions.request({ origins: ["<all_urls>"] }, (granted) => {
63
+ const e = chrome.runtime.lastError;
64
+ $("diag").textContent = e
65
+ ? `request error: ${e.message}`
66
+ : granted
67
+ ? "✓ All-sites access granted. Try 'Test page access'."
68
+ : "✗ Access request was dismissed/denied.";
69
+ });
70
+ });
71
+
72
+ $("test").addEventListener("click", async () => {
73
+ $("diag").textContent = "testing…";
74
+ const res = await send("diag");
75
+ const lines = [];
76
+ lines.push(`manifest host_permissions: ${JSON.stringify(res.hostPermissions)}`);
77
+ lines.push(`granted origins: ${JSON.stringify(res.grantedOrigins)}`);
78
+ lines.push(`tab: ${res.tabUrl || "(none)"}`);
79
+ lines.push(res.okExec ? `✓ executeScript OK → ${res.value}` : `✗ executeScript FAILED: ${res.error}`);
80
+ $("diag").textContent = lines.join("\n");
81
+ });
82
+
83
+ $("autopair").addEventListener("click", async () => {
84
+ $("autopair").disabled = true;
85
+ $("pairmsg").textContent = "Asking the local pairing host…";
86
+ const res = await send("auto-pair");
87
+ if (res.ok) {
88
+ if (res.token) $("token").value = res.token;
89
+ $("pairmsg").textContent = "✓ Paired.";
90
+ renderStatus(res.status);
91
+ } else {
92
+ $("pairmsg").textContent = res.error || "Auto-pair failed — paste the token manually below.";
93
+ }
94
+ $("autopair").disabled = false;
95
+ await refresh();
96
+ });
97
+
98
+ $("save").addEventListener("click", async () => {
99
+ $("save").disabled = true;
100
+ $("save").textContent = "Connecting…";
101
+ const res = await send("set-token", { token: $("token").value.trim() });
102
+ renderStatus(res.status);
103
+ await refresh();
104
+ $("save").disabled = false;
105
+ $("save").textContent = "Save & connect";
106
+ });
107
+
108
+ init();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iframer-toolkit",
3
- "version": "0.9.0",
3
+ "version": "0.10.5",
4
4
  "description": "Browser access toolkit for AI agents — CLI and MCP server for headless/headful browsing, login flows, and pipeline execution.",
5
5
  "keywords": [
6
6
  "iframer",
@@ -29,6 +29,7 @@
29
29
  "files": [
30
30
  "dist/",
31
31
  "skills/",
32
+ "extension/",
32
33
  "README.md",
33
34
  "LICENSE"
34
35
  ],
@@ -44,12 +45,12 @@
44
45
  "test:smoke": "node tests/smoke.js",
45
46
  "mcp:install": "node bin/cli.js install-mcp",
46
47
  "mcp:remove": "node bin/cli.js remove-mcp",
47
- "build:cli": "bun build bin/cli.js --target node --format cjs --outfile dist/cli.js --external better-sqlite3 --external patchright --external fingerprint-generator --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk --external zod --external cloakbrowser --external playwright-core",
48
- "build:mcp": "bun build src/mcp/server.ts --target node --format cjs --outfile dist/mcp-server.cjs --external better-sqlite3 --external patchright --external fingerprint-generator --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk --external zod --external cloakbrowser --external playwright-core",
49
- "build:local-server": "bun build index.ts --target node --format cjs --outfile dist/local-server.cjs --external better-sqlite3 --external patchright --external fingerprint-generator --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk --external zod --external cloakbrowser --external playwright-core --external express",
50
- "build": "bun run build:cli && bun run build:mcp && bun run build:local-server",
48
+ "build:stamp": "node scripts/stamp-extension-version.mjs",
49
+ "build": "bun run build:stamp && node scripts/build.mjs",
51
50
  "test": "bun test tests/unit",
52
- "prepublishOnly": "bun run build"
51
+ "test:extension": "node tests/integration/extension-bridge.mjs",
52
+ "prepublishOnly": "bun run build",
53
+ "test:cdp-relay": "bun tests/integration/cdp-relay.mjs"
53
54
  },
54
55
  "dependencies": {
55
56
  "@anthropic-ai/sdk": "^0.80.0",
@@ -57,6 +58,8 @@
57
58
  "better-sqlite3": "^12.8.0",
58
59
  "fingerprint-generator": "^2.1.81",
59
60
  "patchright": "^1.58.2",
61
+ "playwright-core": "^1.58.0",
62
+ "ws": "^8.18.0",
60
63
  "zod": "^4.3.6"
61
64
  },
62
65
  "optionalDependencies": {
@@ -66,8 +69,11 @@
66
69
  "@types/better-sqlite3": "^7.6.13",
67
70
  "@types/express": "^5.0.6",
68
71
  "@types/node": "^25.5.0",
72
+ "@types/ws": "^8.5.12",
69
73
  "bun-types": "^1.3.11",
74
+ "esbuild": "0.28.2",
70
75
  "express": "^4.18.2",
76
+ "tsx": "^4.23.12",
71
77
  "typescript": "^6.0.2"
72
78
  }
73
79
  }