@1dolinski/fastforms 0.3.0 → 0.4.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.
package/lib/cdp.js ADDED
@@ -0,0 +1,193 @@
1
+ import WebSocket from "ws";
2
+
3
+ const DEFAULT_TIMEOUT = 30_000;
4
+ const SETTLE_MS = 1000;
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // CDP session — thin wrapper around a single WebSocket to a Chrome target
8
+ // ---------------------------------------------------------------------------
9
+
10
+ class CDPSession {
11
+ constructor(ws) {
12
+ this._ws = ws;
13
+ this._nextId = 0;
14
+ this._pending = new Map();
15
+ this._listeners = new Map();
16
+
17
+ ws.on("message", (raw) => {
18
+ const msg = JSON.parse(raw);
19
+ if ("id" in msg) {
20
+ const cb = this._pending.get(msg.id);
21
+ if (cb) {
22
+ this._pending.delete(msg.id);
23
+ msg.error ? cb.reject(new Error(msg.error.message)) : cb.resolve(msg.result);
24
+ }
25
+ } else if (msg.method) {
26
+ const fns = this._listeners.get(msg.method);
27
+ if (fns) for (const fn of fns) fn(msg.params);
28
+ }
29
+ });
30
+
31
+ ws.on("close", () => {
32
+ for (const { reject } of this._pending.values()) reject(new Error("WebSocket closed"));
33
+ this._pending.clear();
34
+ });
35
+ }
36
+
37
+ send(method, params = {}) {
38
+ const id = ++this._nextId;
39
+ return new Promise((resolve, reject) => {
40
+ this._pending.set(id, { resolve, reject });
41
+ this._ws.send(JSON.stringify({ id, method, params }));
42
+ });
43
+ }
44
+
45
+ on(event, fn) {
46
+ if (!this._listeners.has(event)) this._listeners.set(event, []);
47
+ this._listeners.get(event).push(fn);
48
+ }
49
+
50
+ off(event, fn) {
51
+ const fns = this._listeners.get(event);
52
+ if (!fns) return;
53
+ const idx = fns.indexOf(fn);
54
+ if (idx !== -1) fns.splice(idx, 1);
55
+ }
56
+
57
+ close() {
58
+ this._ws.close();
59
+ }
60
+ }
61
+
62
+ async function openSession(wsUrl) {
63
+ const ws = new WebSocket(wsUrl);
64
+ await new Promise((resolve, reject) => {
65
+ ws.once("open", resolve);
66
+ ws.once("error", reject);
67
+ });
68
+ return new CDPSession(ws);
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Page — mirrors the subset of puppeteer's Page API we actually use
73
+ // ---------------------------------------------------------------------------
74
+
75
+ class Page {
76
+ constructor(info, port) {
77
+ this._info = info;
78
+ this._port = port;
79
+ this._session = null;
80
+ }
81
+
82
+ url() {
83
+ return this._info.url;
84
+ }
85
+
86
+ async _ensureSession() {
87
+ if (!this._session) {
88
+ this._session = await openSession(this._info.webSocketDebuggerUrl);
89
+ }
90
+ return this._session;
91
+ }
92
+
93
+ /**
94
+ * Evaluate a function in the page context.
95
+ * Works the same as puppeteer's page.evaluate — pass a function + args,
96
+ * they get serialized and executed in the browser, result comes back.
97
+ */
98
+ async evaluate(fn, ...args) {
99
+ const session = await this._ensureSession();
100
+ const expression = `(${fn.toString()})(${args.map((a) => JSON.stringify(a)).join(",")})`;
101
+ const { result, exceptionDetails } = await session.send("Runtime.evaluate", {
102
+ expression,
103
+ returnByValue: true,
104
+ awaitPromise: true,
105
+ });
106
+ if (exceptionDetails) {
107
+ throw new Error(
108
+ exceptionDetails.text ||
109
+ exceptionDetails.exception?.description ||
110
+ "evaluate failed"
111
+ );
112
+ }
113
+ return result?.value;
114
+ }
115
+
116
+ async goto(url, opts = {}) {
117
+ const session = await this._ensureSession();
118
+ await session.send("Page.enable");
119
+
120
+ const loaded = new Promise((resolve) => {
121
+ const timer = setTimeout(resolve, opts.timeout || DEFAULT_TIMEOUT);
122
+ const handler = () => {
123
+ clearTimeout(timer);
124
+ session.off("Page.loadEventFired", handler);
125
+ setTimeout(resolve, SETTLE_MS);
126
+ };
127
+ session.on("Page.loadEventFired", handler);
128
+ });
129
+
130
+ await session.send("Page.navigate", { url });
131
+ await loaded;
132
+ this._info.url = url;
133
+ }
134
+
135
+ async bringToFront() {
136
+ const session = await this._ensureSession();
137
+ await session.send("Page.bringToFront");
138
+ }
139
+
140
+ async close() {
141
+ this._dispose();
142
+ try {
143
+ await fetch(`http://127.0.0.1:${this._port}/json/close/${this._info.id}`, { method: "PUT" });
144
+ } catch { /* best effort */ }
145
+ }
146
+
147
+ _dispose() {
148
+ if (this._session) {
149
+ this._session.close();
150
+ this._session = null;
151
+ }
152
+ }
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // connectChrome — returns a browser-like object
157
+ // ---------------------------------------------------------------------------
158
+
159
+ export async function connectChrome(port = 9222) {
160
+ const base = `http://127.0.0.1:${port}`;
161
+
162
+ const res = await fetch(`${base}/json/version`);
163
+ if (!res.ok) throw new Error(`Chrome not responding on port ${port}`);
164
+
165
+ const sessions = [];
166
+
167
+ return {
168
+ async pages() {
169
+ const res = await fetch(`${base}/json/list`);
170
+ const targets = await res.json();
171
+ return targets
172
+ .filter((t) => t.type === "page")
173
+ .map((t) => {
174
+ const p = new Page(t, port);
175
+ sessions.push(p);
176
+ return p;
177
+ });
178
+ },
179
+
180
+ async newPage() {
181
+ const res = await fetch(`${base}/json/new`, { method: "PUT" });
182
+ const target = await res.json();
183
+ const p = new Page(target, port);
184
+ sessions.push(p);
185
+ return p;
186
+ },
187
+
188
+ disconnect() {
189
+ for (const p of sessions) p._dispose();
190
+ sessions.length = 0;
191
+ },
192
+ };
193
+ }
package/lib/personas.js CHANGED
@@ -1,8 +1,8 @@
1
- import puppeteer from "puppeteer-core";
2
- import { readFileSync, writeFileSync, existsSync } from "fs";
1
+ import { readFileSync, writeFileSync } from "fs";
3
2
  import { join } from "path";
4
3
  import { homedir } from "os";
5
4
  import { createInterface } from "readline";
5
+ import { connectChrome } from "./cdp.js";
6
6
 
7
7
  const FASTFORMS_URL = "https://293-fastforms.vercel.app/";
8
8
  const PERSONA_URL = "https://293-fastforms.vercel.app/persona";
@@ -28,7 +28,7 @@ function ask(prompt) {
28
28
  }
29
29
 
30
30
  export async function connectToChrome(port = 9222) {
31
- return puppeteer.connect({ browserURL: `http://127.0.0.1:${port}` });
31
+ return connectChrome(port);
32
32
  }
33
33
 
34
34
  export async function pullPersonas(browser) {
@@ -121,26 +121,23 @@ export function showPersonaDetails(user, biz, form) {
121
121
  }
122
122
 
123
123
  // ---------------------------------------------------------------------------
124
- // Interactive persona selection — supports multiple personas from any source
124
+ // Interactive persona selection
125
125
  // ---------------------------------------------------------------------------
126
126
 
127
127
  async function pickFromList(label, list, hint, keys, defaultName) {
128
128
  if (!list?.length) return null;
129
129
  if (list.length === 1) return list[0];
130
130
 
131
- // Try hint-based match
132
131
  if (hint) {
133
132
  const match = pickPersona(list, hint, keys);
134
133
  if (match) return match;
135
134
  }
136
135
 
137
- // Try default
138
136
  if (defaultName) {
139
137
  const match = list.find((p) => p.name === defaultName);
140
138
  if (match) return match;
141
139
  }
142
140
 
143
- // Interactive selection
144
141
  console.log(`\n ${label}:\n`);
145
142
  list.forEach((p, i) => {
146
143
  const detail = keys.map((k) => k.split(".").reduce((o, s) => o?.[s], p)).filter(Boolean)[0] || "";
@@ -168,7 +165,6 @@ export function matchFormByUrl(formPersonas, targetUrl) {
168
165
  export async function selectFormPersona(formPersonas, targetUrl) {
169
166
  if (!formPersonas?.length) return null;
170
167
 
171
- // Auto-match by URL
172
168
  const autoMatch = matchFormByUrl(formPersonas, targetUrl);
173
169
  if (autoMatch) {
174
170
  console.log(` Auto-matched form persona: ${autoMatch.name}`);
@@ -177,7 +173,6 @@ export async function selectFormPersona(formPersonas, targetUrl) {
177
173
 
178
174
  if (formPersonas.length === 1) return formPersonas[0];
179
175
 
180
- // Interactive
181
176
  console.log(`\n Form personas:\n`);
182
177
  formPersonas.forEach((f, i) => {
183
178
  const org = f.profile?.organization || "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1dolinski/fastforms",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Fill any form fast. Manage personas on the web, fill forms from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "README.md"
14
14
  ],
15
15
  "dependencies": {
16
- "puppeteer-core": "^24.39.1"
16
+ "ws": "^8.18.0"
17
17
  },
18
18
  "engines": {
19
19
  "node": ">=18"
@@ -27,7 +27,7 @@
27
27
  "forms",
28
28
  "automation",
29
29
  "chrome",
30
- "puppeteer",
30
+ "cdp",
31
31
  "personas"
32
32
  ]
33
33
  }