nothing-browser 0.0.1

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,330 @@
1
+ // piggy/client/index.ts
2
+ import { connect, type Socket } from "net";
3
+ import { writeFileSync, mkdirSync } from "fs";
4
+ import { dirname } from "path";
5
+ import logger from "../logger";
6
+
7
+ export class PiggyClient {
8
+ private socketPath: string;
9
+ private socket: Socket | null = null;
10
+ private reqId = 0;
11
+ private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
12
+ private buf = "";
13
+
14
+ constructor(socketPath = "/tmp/piggy") {
15
+ this.socketPath = socketPath;
16
+ }
17
+
18
+ connect(): Promise<void> {
19
+ return new Promise((resolve, reject) => {
20
+ logger.info(`Connecting to socket: ${this.socketPath}`);
21
+ const sock = connect(this.socketPath);
22
+ sock.setEncoding("utf8");
23
+
24
+ sock.on("connect", () => {
25
+ this.socket = sock;
26
+ logger.success("Connected to Piggy server");
27
+ resolve();
28
+ });
29
+
30
+ sock.on("data", (chunk: string) => {
31
+ this.buf += chunk;
32
+ const lines = this.buf.split("\n");
33
+ this.buf = lines.pop()!;
34
+ for (const line of lines) {
35
+ if (!line.trim()) continue;
36
+ try {
37
+ const msg = JSON.parse(line);
38
+ const p = this.pending.get(msg.id);
39
+ if (p) {
40
+ this.pending.delete(msg.id);
41
+ msg.ok ? p.resolve(msg.data) : p.reject(new Error(msg.data ?? "command failed"));
42
+ }
43
+ } catch {
44
+ logger.error(`Bad JSON from server: ${line}`);
45
+ }
46
+ }
47
+ });
48
+
49
+ sock.on("error", (e) => {
50
+ for (const p of this.pending.values()) p.reject(e);
51
+ this.pending.clear();
52
+ reject(e);
53
+ });
54
+
55
+ sock.on("close", () => {
56
+ for (const p of this.pending.values()) p.reject(new Error("Socket closed"));
57
+ this.pending.clear();
58
+ });
59
+ });
60
+ }
61
+
62
+ disconnect() {
63
+ this.socket?.destroy();
64
+ this.socket = null;
65
+ }
66
+
67
+ private send<T = any>(cmd: string, payload: Record<string, any> = {}): Promise<T> {
68
+ return new Promise((resolve, reject) => {
69
+ if (!this.socket) return reject(new Error("Not connected"));
70
+ const id = String(++this.reqId);
71
+ this.pending.set(id, { resolve, reject });
72
+ this.socket.write(JSON.stringify({ id, cmd, payload }) + "\n");
73
+ });
74
+ }
75
+
76
+ // ── Tabs ─────────────────────────────────────────────────────────────────────
77
+
78
+ async newTab(): Promise<string> {
79
+ return this.send<string>("tab.new", {});
80
+ }
81
+
82
+ async closeTab(tabId: string): Promise<void> {
83
+ await this.send("tab.close", { tabId });
84
+ }
85
+
86
+ async listTabs(): Promise<string[]> {
87
+ return this.send<string[]>("tab.list", {});
88
+ }
89
+
90
+ // ── Navigation ───────────────────────────────────────────────────────────────
91
+
92
+ async navigate(url: string, tabId = "default"): Promise<void> {
93
+ await this.send("navigate", { url, tabId });
94
+ }
95
+
96
+ async reload(tabId = "default"): Promise<void> {
97
+ await this.send("reload", { tabId });
98
+ }
99
+
100
+ async goBack(tabId = "default"): Promise<void> {
101
+ await this.send("go.back", { tabId });
102
+ }
103
+
104
+ async goForward(tabId = "default"): Promise<void> {
105
+ await this.send("go.forward", { tabId });
106
+ }
107
+
108
+ // ── Page info ─────────────────────────────────────────────────────────────────
109
+
110
+ async getTitle(tabId = "default"): Promise<string> {
111
+ return this.send<string>("page.title", { tabId });
112
+ }
113
+
114
+ async getUrl(tabId = "default"): Promise<string> {
115
+ return this.send<string>("page.url", { tabId });
116
+ }
117
+
118
+ async content(tabId = "default"): Promise<string> {
119
+ return this.send<string>("page.content", { tabId });
120
+ }
121
+
122
+ // ── Eval / JS ─────────────────────────────────────────────────────────────────
123
+
124
+ async evaluate(js: string, tabId = "default"): Promise<any> {
125
+ return this.send("evaluate", { js, tabId });
126
+ }
127
+
128
+ // ── Interactions ──────────────────────────────────────────────────────────────
129
+
130
+ async click(selector: string, tabId = "default"): Promise<boolean> {
131
+ return this.send<boolean>("click", { selector, tabId });
132
+ }
133
+
134
+ async doubleClick(selector: string, tabId = "default"): Promise<boolean> {
135
+ return this.send<boolean>("dblclick", { selector, tabId });
136
+ }
137
+
138
+ async hover(selector: string, tabId = "default"): Promise<boolean> {
139
+ return this.send<boolean>("hover", { selector, tabId });
140
+ }
141
+
142
+ async type(selector: string, text: string, tabId = "default"): Promise<boolean> {
143
+ return this.send<boolean>("type", { selector, text, tabId });
144
+ }
145
+
146
+ async select(selector: string, value: string, tabId = "default"): Promise<boolean> {
147
+ return this.send<boolean>("select", { selector, value, tabId });
148
+ }
149
+
150
+ async keyPress(key: string, tabId = "default"): Promise<boolean> {
151
+ return this.send<boolean>("keyboard.press", { key, tabId });
152
+ }
153
+
154
+ async keyCombo(combo: string, tabId = "default"): Promise<boolean> {
155
+ return this.send<boolean>("keyboard.combo", { combo, tabId });
156
+ }
157
+
158
+ async mouseMove(x: number, y: number, tabId = "default"): Promise<boolean> {
159
+ return this.send<boolean>("mouse.move", { x, y, tabId });
160
+ }
161
+
162
+ async mouseDrag(from: { x: number; y: number }, to: { x: number; y: number }, tabId = "default"): Promise<boolean> {
163
+ return this.send<boolean>("mouse.drag", { from, to, tabId });
164
+ }
165
+
166
+ // ── Scroll ────────────────────────────────────────────────────────────────────
167
+
168
+ async scrollTo(selector: string, tabId = "default"): Promise<boolean> {
169
+ return this.send<boolean>("scroll.to", { selector, tabId });
170
+ }
171
+
172
+ async scrollBy(px: number, tabId = "default"): Promise<boolean> {
173
+ return this.send<boolean>("scroll.by", { px, tabId });
174
+ }
175
+
176
+ // ── Fetch ─────────────────────────────────────────────────────────────────────
177
+
178
+ async fetchText(query: string, tabId = "default"): Promise<string | null> {
179
+ return this.send<string | null>("fetch.text", { query, tabId });
180
+ }
181
+
182
+ async fetchLinks(query: string, tabId = "default"): Promise<string[]> {
183
+ if (query === "a" || query === "body") {
184
+ const result = await this.send<string[]>("fetch.links.all", { tabId });
185
+ return Array.isArray(result) ? result : [];
186
+ }
187
+ const result = await this.send<string[]>("fetch.links", { query, tabId });
188
+ return Array.isArray(result) ? result : [];
189
+ }
190
+
191
+ async fetchImages(query: string, tabId = "default"): Promise<string[]> {
192
+ const result = await this.send<string[]>("fetch.image", { query, tabId });
193
+ return Array.isArray(result) ? result : [];
194
+ }
195
+
196
+ // ── Search ────────────────────────────────────────────────────────────────────
197
+
198
+ async searchCss(query: string, tabId = "default"): Promise<any> {
199
+ return this.send("search.css", { query, tabId });
200
+ }
201
+
202
+ async searchId(query: string, tabId = "default"): Promise<any> {
203
+ return this.send("search.id", { query, tabId });
204
+ }
205
+
206
+ // ── Wait ──────────────────────────────────────────────────────────────────────
207
+
208
+ async waitForSelector(selector: string, timeout = 30000, tabId = "default"): Promise<void> {
209
+ await this.send("wait.selector", { selector, timeout, tabId });
210
+ }
211
+
212
+ async waitForNavigation(tabId = "default"): Promise<void> {
213
+ await this.send("wait.navigation", { tabId });
214
+ }
215
+
216
+ async waitForResponse(urlPattern: string, timeout = 30000, tabId = "default"): Promise<void> {
217
+ await this.send("wait.response", { url: urlPattern, timeout, tabId });
218
+ }
219
+
220
+ // ── Screenshot ────────────────────────────────────────────────────────────────
221
+
222
+ async screenshot(filePath?: string, tabId = "default"): Promise<string> {
223
+ const b64 = await this.send<string>("screenshot", { tabId });
224
+ if (filePath) {
225
+ mkdirSync(dirname(filePath), { recursive: true });
226
+ writeFileSync(filePath, Buffer.from(b64, "base64"));
227
+ }
228
+ return filePath ?? b64;
229
+ }
230
+
231
+ // ── PDF ───────────────────────────────────────────────────────────────────────
232
+
233
+ async pdf(filePath?: string, tabId = "default"): Promise<string> {
234
+ const b64 = await this.send<string>("pdf", { tabId });
235
+ if (filePath) {
236
+ mkdirSync(dirname(filePath), { recursive: true });
237
+ writeFileSync(filePath, Buffer.from(b64, "base64"));
238
+ }
239
+ return filePath ?? b64;
240
+ }
241
+
242
+ // ── Image blocking ────────────────────────────────────────────────────────────
243
+
244
+ async blockImages(tabId = "default"): Promise<void> {
245
+ await this.send("intercept.block.images", { tabId });
246
+ }
247
+
248
+ async unblockImages(tabId = "default"): Promise<void> {
249
+ await this.send("intercept.unblock.images", { tabId });
250
+ }
251
+
252
+ // ── Cookies ───────────────────────────────────────────────────────────────────
253
+
254
+ async setCookie(
255
+ name: string,
256
+ value: string,
257
+ domain: string,
258
+ path = "/",
259
+ tabId = "default"
260
+ ): Promise<void> {
261
+ await this.send("cookie.set", { name, value, domain, path, tabId });
262
+ }
263
+
264
+ async getCookie(name: string, tabId = "default"): Promise<any> {
265
+ return this.send("cookie.get", { name, tabId });
266
+ }
267
+
268
+ async deleteCookie(name: string, tabId = "default"): Promise<void> {
269
+ await this.send("cookie.delete", { name, tabId });
270
+ }
271
+
272
+ async listCookies(tabId = "default"): Promise<any[]> {
273
+ return this.send<any[]>("cookie.list", { tabId });
274
+ }
275
+
276
+ // ── Interception ──────────────────────────────────────────────────────────────
277
+
278
+ async addInterceptRule(
279
+ action: "block" | "redirect" | "modifyHeaders",
280
+ pattern: string,
281
+ options: { redirectUrl?: string; headers?: Record<string, string> } = {},
282
+ tabId = "default"
283
+ ): Promise<void> {
284
+ await this.send("intercept.rule.add", { action, pattern, ...options, tabId });
285
+ }
286
+
287
+ async clearInterceptRules(tabId = "default"): Promise<void> {
288
+ await this.send("intercept.rule.clear", { tabId });
289
+ }
290
+
291
+ // ── Network capture ───────────────────────────────────────────────────────────
292
+
293
+ async captureStart(tabId = "default"): Promise<void> {
294
+ await this.send("capture.start", { tabId });
295
+ }
296
+
297
+ async captureStop(tabId = "default"): Promise<void> {
298
+ await this.send("capture.stop", { tabId });
299
+ }
300
+
301
+ async captureRequests(tabId = "default"): Promise<any[]> {
302
+ return this.send<any[]>("capture.requests", { tabId });
303
+ }
304
+
305
+ async captureWs(tabId = "default"): Promise<any[]> {
306
+ return this.send<any[]>("capture.ws", { tabId });
307
+ }
308
+
309
+ async captureCookies(tabId = "default"): Promise<any[]> {
310
+ return this.send<any[]>("capture.cookies", { tabId });
311
+ }
312
+
313
+ async captureStorage(tabId = "default"): Promise<any> {
314
+ return this.send("capture.storage", { tabId });
315
+ }
316
+
317
+ async captureClear(tabId = "default"): Promise<void> {
318
+ await this.send("capture.clear", { tabId });
319
+ }
320
+
321
+ // ── Session ───────────────────────────────────────────────────────────────────
322
+
323
+ async sessionExport(tabId = "default"): Promise<any> {
324
+ return this.send("session.export", { tabId });
325
+ }
326
+
327
+ async sessionImport(data: any, tabId = "default"): Promise<void> {
328
+ await this.send("session.import", { data, tabId });
329
+ }
330
+ }
@@ -0,0 +1,53 @@
1
+ // piggy/human/index.ts
2
+
3
+ export function randomDelay(min: number, max: number): Promise<void> {
4
+ return new Promise(r => setTimeout(r, Math.floor(Math.random() * (max - min + 1)) + min));
5
+ }
6
+
7
+ /**
8
+ * Simulates human typing by introducing ~2 random typos and correcting them.
9
+ * Returns an array of { char, isBackspace } actions to replay.
10
+ */
11
+ export function humanTypeSequence(text: string): string[] {
12
+ const adjacent: Record<string, string[]> = {
13
+ a: ["q","w","s","z"], b: ["v","g","h","n"], c: ["x","d","f","v"],
14
+ d: ["s","e","r","f","c","x"], e: ["w","r","d","s"],
15
+ f: ["d","r","t","g","v","c"], g: ["f","t","y","h","b","v"],
16
+ h: ["g","y","u","j","n","b"], i: ["u","o","k","j"],
17
+ j: ["h","u","i","k","m","n"], k: ["j","i","o","l","m"],
18
+ l: ["k","o","p"], m: ["n","j","k"], n: ["b","h","j","m"],
19
+ o: ["i","p","l","k"], p: ["o","l"], q: ["w","a"],
20
+ r: ["e","t","f","d"], s: ["a","w","e","d","x","z"],
21
+ t: ["r","y","g","f"], u: ["y","i","h","j"],
22
+ v: ["c","f","g","b"], w: ["q","e","a","s"],
23
+ x: ["z","s","d","c"], y: ["t","u","g","h"],
24
+ z: ["a","s","x"],
25
+ };
26
+
27
+ const actions: string[] = [];
28
+ const typoIndices = new Set<number>();
29
+
30
+ // Pick ~2 random positions to typo (skip short strings)
31
+ if (text.length > 4) {
32
+ let tries = 0;
33
+ while (typoIndices.size < 2 && tries < 20) {
34
+ typoIndices.add(Math.floor(Math.random() * text.length));
35
+ tries++;
36
+ }
37
+ }
38
+
39
+ for (let i = 0; i < text.length; i++) {
40
+ if (typoIndices.has(i)) {
41
+ const ch = text[i]!.toLowerCase();
42
+ const neighbors = adjacent[ch];
43
+ const typo = neighbors
44
+ ? neighbors[Math.floor(Math.random() * neighbors.length)] ?? ch
45
+ : ch;
46
+ actions.push(typo); // wrong char
47
+ actions.push("BACKSPACE"); // correct it
48
+ }
49
+ actions.push(text[i]!);
50
+ }
51
+
52
+ return actions;
53
+ }
@@ -0,0 +1,33 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import logger from '../logger';
4
+
5
+ export function detectBinary(): string | null {
6
+ const cwd = process.cwd();
7
+
8
+ // Windows
9
+ const windowsPath = join(cwd, 'nothing-browser-headless.exe');
10
+ if (process.platform === 'win32' && existsSync(windowsPath)) {
11
+ logger.success(`Binary found! Platform: Windows`);
12
+ return windowsPath;
13
+ }
14
+
15
+ // Linux / macOS
16
+ const unixPath = join(cwd, 'nothing-browser-headless');
17
+ if (existsSync(unixPath)) {
18
+ logger.success(`Binary found at: ${unixPath}`);
19
+ return unixPath;
20
+ }
21
+
22
+ logger.error("❌ Binary not found in project root");
23
+ logger.error("");
24
+ logger.error("Download from:");
25
+ logger.error(" https://github.com/BunElysiaReact/nothing-browser/releases/");
26
+ logger.error("");
27
+ logger.error(`Place in: ${cwd}/nothing-browser-headless${process.platform === 'win32' ? '.exe' : ''}`);
28
+ if (process.platform !== 'win32') {
29
+ logger.error("Then run: chmod +x nothing-browser-headless");
30
+ }
31
+
32
+ return null;
33
+ }
@@ -0,0 +1,101 @@
1
+ import { spawn } from 'bun';
2
+ import { execSync } from 'child_process';
3
+ import { detectBinary } from './detect';
4
+ import logger from '../logger';
5
+
6
+ let activeProcess: any = null;
7
+ const extraProcesses: any[] = [];
8
+
9
+ export function killAllBrowsers(): void {
10
+ try {
11
+ logger.info("Cleaning up existing browser processes...");
12
+ execSync('pkill -f nothing-browser-headless 2>/dev/null || true', { stdio: 'ignore' });
13
+ execSync('pkill -f QtWebEngineProcess 2>/dev/null || true', { stdio: 'ignore' });
14
+ execSync('rm -f /tmp/piggy', { stdio: 'ignore' });
15
+ } catch {
16
+ // Ignore errors - no processes to kill
17
+ }
18
+ }
19
+
20
+ export async function spawnBrowser(): Promise<string> {
21
+ killAllBrowsers();
22
+
23
+ // Give OS time to release the socket
24
+ await new Promise(resolve => setTimeout(resolve, 500));
25
+
26
+ const binaryPath = detectBinary();
27
+ if (!binaryPath) {
28
+ throw new Error("Binary not found. Cannot launch.");
29
+ }
30
+
31
+ logger.info(`Spawning Nothing Browser from: ${binaryPath}`);
32
+
33
+ activeProcess = spawn([binaryPath], {
34
+ stdio: ['ignore', 'pipe', 'pipe'],
35
+ env: process.env
36
+ });
37
+
38
+ if (activeProcess.stdout) {
39
+ const reader = activeProcess.stdout.getReader();
40
+ const read = async () => {
41
+ const { done, value } = await reader.read();
42
+ if (!done) {
43
+ const output = new TextDecoder().decode(value);
44
+ logger.debug(`[Browser] ${output}`);
45
+ read();
46
+ }
47
+ };
48
+ read();
49
+ }
50
+
51
+ activeProcess.exited.then((code: number | null) => {
52
+ logger.warn(`Browser process exited with code: ${code}`);
53
+ activeProcess = null;
54
+ });
55
+
56
+ await new Promise(resolve => setTimeout(resolve, 2000));
57
+
58
+ if (activeProcess) {
59
+ logger.success("Browser spawned and running");
60
+ } else {
61
+ logger.error("Browser started but exited immediately");
62
+ }
63
+
64
+ return binaryPath;
65
+ }
66
+
67
+ export async function spawnBrowserOnSocket(socketName: string): Promise<void> {
68
+ const binaryPath = detectBinary();
69
+ if (!binaryPath) {
70
+ throw new Error("Binary not found. Cannot launch.");
71
+ }
72
+
73
+ logger.info(`Spawning browser on socket: ${socketName}`);
74
+
75
+ const proc = spawn([binaryPath], {
76
+ stdio: ['ignore', 'pipe', 'pipe'],
77
+ env: { ...process.env, PIGGY_SOCKET: socketName }
78
+ });
79
+
80
+ extraProcesses.push(proc);
81
+
82
+ proc.exited.then((code: number | null) => {
83
+ logger.warn(`Browser on socket ${socketName} exited with code: ${code}`);
84
+ });
85
+
86
+ await new Promise(resolve => setTimeout(resolve, 1000));
87
+ logger.success(`Browser spawned on socket: ${socketName}`);
88
+ }
89
+
90
+ export function killBrowser(): void {
91
+ if (activeProcess) {
92
+ logger.info("Killing browser process...");
93
+ activeProcess.kill();
94
+ activeProcess = null;
95
+ }
96
+
97
+ for (const proc of extraProcesses) {
98
+ proc.kill();
99
+ }
100
+ extraProcesses.length = 0;
101
+ }
@@ -0,0 +1,59 @@
1
+
2
+ //piggy/logger/index.ts
3
+ import { createLogger } from "ernest-logger";
4
+
5
+ const logger = createLogger({
6
+ time: true,
7
+ file: true,
8
+ filePath: './piggy.log',
9
+ prefix: '[PIGGY]',
10
+ emoji: true,
11
+ level: 'trace',
12
+ customLevels: {
13
+ info: {
14
+ color: 'blue',
15
+ emoji: 'ℹ️',
16
+ priority: 2
17
+ },
18
+ success: {
19
+ color: 'green',
20
+ emoji: '✅',
21
+ priority: 2
22
+ },
23
+ error: {
24
+ color: 'red',
25
+ emoji: '❌',
26
+ priority: 5
27
+ },
28
+ warn: {
29
+ color: 'yellow',
30
+ emoji: '⚠️',
31
+ priority: 4
32
+ },
33
+ debug: {
34
+ color: 'magenta',
35
+ emoji: '🐞',
36
+ priority: 1
37
+ },
38
+ network: {
39
+ color: 'brightCyan',
40
+ emoji: '🌐',
41
+ priority: 2
42
+ },
43
+ db: {
44
+ color: 'brightMagenta',
45
+ emoji: '🗄️',
46
+ priority: 2
47
+ },
48
+ security: {
49
+ color: 'brightRed',
50
+ emoji: '🔒',
51
+ priority: 3
52
+ }
53
+ }
54
+ });
55
+
56
+ // Now these methods exist with your custom colors
57
+ logger.info("logger initialized"); // Blue with ℹ️
58
+
59
+ export default logger;
@@ -0,0 +1,5 @@
1
+ export default function open() {
2
+ return {
3
+ name: 'piggy-open',
4
+ };
5
+ }