nothing-browser 0.0.20 → 0.1.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 (76) hide show
  1. package/LICENSE +1 -1
  2. package/dist/human/index.js +25 -1
  3. package/dist/launch/detect.js +13 -0
  4. package/dist/launch/spawn.js +13 -0
  5. package/dist/piggy/captcha/index.d.ts +39 -0
  6. package/dist/piggy/captcha/index.d.ts.map +1 -0
  7. package/dist/piggy/capture/index.d.ts +48 -0
  8. package/dist/piggy/capture/index.d.ts.map +1 -0
  9. package/dist/piggy/dialog/index.d.ts +28 -0
  10. package/dist/piggy/dialog/index.d.ts.map +1 -0
  11. package/dist/piggy/export/index.d.ts +62 -0
  12. package/dist/piggy/export/index.d.ts.map +1 -0
  13. package/dist/piggy/find/index.d.ts +90 -0
  14. package/dist/piggy/find/index.d.ts.map +1 -0
  15. package/dist/piggy/http/index.d.ts +14 -0
  16. package/dist/piggy/http/index.d.ts.map +1 -0
  17. package/dist/piggy/human/index.d.ts +36 -4
  18. package/dist/piggy/human/index.d.ts.map +1 -1
  19. package/dist/piggy/iframe/index.d.ts +53 -0
  20. package/dist/piggy/iframe/index.d.ts.map +1 -0
  21. package/dist/piggy/interactions/index.d.ts +24 -0
  22. package/dist/piggy/interactions/index.d.ts.map +1 -0
  23. package/dist/piggy/launch/detect.d.ts +1 -1
  24. package/dist/piggy/launch/detect.d.ts.map +1 -1
  25. package/dist/piggy/launch/spawn.d.ts.map +1 -1
  26. package/dist/piggy/media/index.d.ts +11 -0
  27. package/dist/piggy/media/index.d.ts.map +1 -0
  28. package/dist/piggy/navigation/index.d.ts +16 -0
  29. package/dist/piggy/navigation/index.d.ts.map +1 -0
  30. package/dist/piggy/provide/index.d.ts +81 -0
  31. package/dist/piggy/provide/index.d.ts.map +1 -0
  32. package/dist/piggy/proxy/index.d.ts +94 -0
  33. package/dist/piggy/proxy/index.d.ts.map +1 -0
  34. package/dist/piggy/register/index.d.ts.map +1 -1
  35. package/dist/piggy/router/index.d.ts +38 -0
  36. package/dist/piggy/router/index.d.ts.map +1 -0
  37. package/dist/piggy/session/index.d.ts +27 -0
  38. package/dist/piggy/session/index.d.ts.map +1 -0
  39. package/dist/piggy/tabs/index.d.ts +10 -0
  40. package/dist/piggy/tabs/index.d.ts.map +1 -0
  41. package/dist/piggy/wait/index.d.ts +28 -0
  42. package/dist/piggy/wait/index.d.ts.map +1 -0
  43. package/dist/piggy.d.ts.map +1 -1
  44. package/dist/piggy.js +914 -181
  45. package/dist/register/index.js +39 -86
  46. package/package.json +1 -1
  47. package/piggy/captcha/index.d.ts +35 -0
  48. package/piggy/captcha/index.ts +93 -0
  49. package/piggy/capture/index.ts +76 -0
  50. package/piggy/dialog/index.d.ts +29 -0
  51. package/piggy/dialog/index.ts +85 -0
  52. package/piggy/export/index.d.ts +117 -0
  53. package/piggy/export/index.ts +147 -0
  54. package/piggy/find/index.d.ts +79 -0
  55. package/piggy/find/index.ts +165 -0
  56. package/piggy/http/index.ts +65 -0
  57. package/piggy/human/index.ts +115 -53
  58. package/piggy/iframe/index.ts +79 -0
  59. package/piggy/interactions/index.ts +79 -0
  60. package/piggy/launch/detect.ts +19 -2
  61. package/piggy/launch/spawn.ts +1 -8
  62. package/piggy/media/index.ts +46 -0
  63. package/piggy/navigation/index.ts +52 -0
  64. package/piggy/provide/index.ts +144 -0
  65. package/piggy/proxy/index.ts +154 -0
  66. package/piggy/register/index.ts +41 -59
  67. package/piggy/router/index.ts +69 -0
  68. package/piggy/session/index.ts +76 -0
  69. package/piggy/tabs/index.ts +22 -0
  70. package/piggy/wait/index.ts +90 -0
  71. package/piggy.ts +94 -39
  72. package/dist/piggy/open/index.d.ts +0 -4
  73. package/dist/piggy/open/index.d.ts.map +0 -1
  74. package/piggy/open/index.d.ts +0 -4
  75. package/piggy/open/index.d.ts.map +0 -1
  76. package/piggy/open/index.ts +0 -5
@@ -0,0 +1,147 @@
1
+ // piggy/export/index.ts
2
+ import { PiggyClient } from "../client";
3
+ import type { CapturedRequest, WebSocketFrame, CapturedCookie } from "../capture";
4
+ import type { SessionPaths } from "../session";
5
+
6
+ // ─── Intercept ────────────────────────────────────────────────────────────────
7
+ export interface InterceptRule {
8
+ pattern: string;
9
+ block?: boolean;
10
+ redirect?: string;
11
+ setHeaders?: Record<string, string>;
12
+ removeHeaders?: string[];
13
+ }
14
+
15
+ // ─── Cookie types (defined here, imported by session) ────────────────────────
16
+ export interface CookieSetOptions {
17
+ name: string;
18
+ value: string;
19
+ domain: string;
20
+ path?: string;
21
+ httpOnly?: boolean;
22
+ secure?: boolean;
23
+ expiry?: number;
24
+ }
25
+
26
+ export interface CookieDeleteOptions {
27
+ name: string;
28
+ domain: string;
29
+ }
30
+
31
+ // ─── Session export types (re-exported from capture) ────────────────────────
32
+ export type { CapturedRequest, WebSocketFrame, CapturedCookie, SessionPaths };
33
+
34
+ export interface SessionExport {
35
+ url: string;
36
+ requests: CapturedRequest[];
37
+ ws: WebSocketFrame[];
38
+ cookies: CapturedCookie[];
39
+ }
40
+
41
+ export interface ExposedFunctionCall {
42
+ name: string;
43
+ callId: string;
44
+ data: string;
45
+ tabId: string;
46
+ }
47
+
48
+ // ─── ExportClient ─────────────────────────────────────────────────────────────
49
+ export class ExportClient {
50
+ constructor(private client: PiggyClient) {}
51
+
52
+ // DOM fetch
53
+ searchCss(query: string, tabId = "default"): Promise<any> {
54
+ return this.client.send("search.css", { query, tabId });
55
+ }
56
+
57
+ searchId(query: string, tabId = "default"): Promise<any> {
58
+ return this.client.send("search.id", { query, tabId });
59
+ }
60
+
61
+ // Cookies
62
+ setCookie(opts: CookieSetOptions, tabId = "default"): Promise<void> {
63
+ return this.client.send("cookie.set", { ...opts, tabId });
64
+ }
65
+
66
+ deleteCookie(opts: CookieDeleteOptions, tabId = "default"): Promise<void> {
67
+ return this.client.send("cookie.delete", { ...opts, tabId });
68
+ }
69
+
70
+ // Session
71
+ sessionReload(tabId = "default"): Promise<void> {
72
+ return this.client.send("session.reload", { tabId });
73
+ }
74
+
75
+ cookiesPath(): Promise<string> {
76
+ return this.client.send("session.cookies.path", {});
77
+ }
78
+
79
+ profilePath(): Promise<string> {
80
+ return this.client.send("session.profile.path", {});
81
+ }
82
+
83
+ wsPath(): Promise<string> {
84
+ return this.client.send("session.ws.path", {});
85
+ }
86
+
87
+ pingsPath(): Promise<string> {
88
+ return this.client.send("session.pings.path", {});
89
+ }
90
+
91
+ sessionPaths(): Promise<SessionPaths> {
92
+ return this.client.send("session.paths", {});
93
+ }
94
+
95
+ setWsSave(enabled: boolean): Promise<void> {
96
+ return this.client.send("session.ws.save", { enabled });
97
+ }
98
+
99
+ setPingsSave(enabled: boolean): Promise<void> {
100
+ return this.client.send("session.pings.save", { enabled });
101
+ }
102
+
103
+ // Intercept rules
104
+ addInterceptRule(rule: InterceptRule, tabId = "default"): Promise<void> {
105
+ return this.client.send("intercept.rule.add", { ...rule, tabId });
106
+ }
107
+
108
+ clearInterceptRules(tabId = "default"): Promise<void> {
109
+ return this.client.send("intercept.rule.clear", { tabId });
110
+ }
111
+
112
+ // Session export / import
113
+ async exportSession(tabId = "default"): Promise<SessionExport> {
114
+ const raw = await this.client.send<string>("session.export", { tabId });
115
+ return typeof raw === "string" ? JSON.parse(raw) : raw;
116
+ }
117
+
118
+ importSession(data: SessionExport, tabId = "default"): Promise<void> {
119
+ return this.client.send("session.import", {
120
+ data: JSON.stringify(data),
121
+ tabId,
122
+ });
123
+ }
124
+
125
+ // Exposed functions
126
+ exposeFunction(name: string, tabId = "default"): Promise<void> {
127
+ return this.client.send("expose.function", { name, tabId });
128
+ }
129
+
130
+ resolveExposed(callId: string, result: string, isError = false, tabId = "default"): Promise<void> {
131
+ return this.client.send("exposed.result", { callId, result, isError, tabId });
132
+ }
133
+
134
+ // Init scripts
135
+ addInitScript(js: string, tabId = "default"): Promise<void> {
136
+ return this.client.send("addInitScript", { js, tabId });
137
+ }
138
+
139
+ // Events
140
+ onExposedFunctionCalled(tabId: string, handler: (call: ExposedFunctionCall) => void): () => void {
141
+ return this.client.onEvent("exposed_call", tabId, handler);
142
+ }
143
+ }
144
+
145
+ export function createExportAPI(client: PiggyClient): ExportClient {
146
+ return new ExportClient(client);
147
+ }
@@ -0,0 +1,79 @@
1
+ // piggy/find/index.d.ts
2
+ import { PiggyClient } from "../client";
3
+
4
+ // ─── Element descriptor ───────────────────────────────────────────────────────
5
+
6
+ export interface ElementDescriptor {
7
+ tag: string;
8
+ id: string;
9
+ cls: string;
10
+ /** First 400 chars of innerText */
11
+ text: string;
12
+ /** First 800 chars of innerHTML */
13
+ html: string;
14
+ href: string;
15
+ src: string;
16
+ value: string;
17
+ attrs: Record<string, string>;
18
+ }
19
+
20
+ // ─── Option types ─────────────────────────────────────────────────────────────
21
+
22
+ export interface FindByTextOptions {
23
+ text: string;
24
+ selector?: string;
25
+ exact?: boolean;
26
+ }
27
+
28
+ export interface FindByAttrOptions {
29
+ attr: string;
30
+ value?: string;
31
+ selector?: string;
32
+ }
33
+
34
+ export interface FindByRoleOptions {
35
+ role: string;
36
+ name?: string;
37
+ }
38
+
39
+ export interface FindClosestOptions {
40
+ selector: string;
41
+ ancestor: string;
42
+ }
43
+
44
+ export interface FindFilterOptions {
45
+ selector: string;
46
+ attr: string;
47
+ value: string;
48
+ }
49
+
50
+ // ─── FindClient ───────────────────────────────────────────────────────────────
51
+
52
+ export declare class FindClient {
53
+ constructor(client: PiggyClient);
54
+
55
+ // Multi-result
56
+ css(selector: string, tabId?: string): Promise<ElementDescriptor[]>;
57
+ all(selector: string, tabId?: string): Promise<ElementDescriptor[]>;
58
+ first(selector: string, tabId?: string): Promise<ElementDescriptor[]>;
59
+ byText(opts: FindByTextOptions, tabId?: string): Promise<ElementDescriptor[]>;
60
+ byAttr(opts: FindByAttrOptions, tabId?: string): Promise<ElementDescriptor[]>;
61
+ byTag(tag: string, tabId?: string): Promise<ElementDescriptor[]>;
62
+ byPlaceholder(text: string, tabId?: string): Promise<ElementDescriptor[]>;
63
+ byRole(opts: FindByRoleOptions, tabId?: string): Promise<ElementDescriptor[]>;
64
+ children(selector: string, tabId?: string): Promise<ElementDescriptor[]>;
65
+ filter(opts: FindFilterOptions, tabId?: string): Promise<ElementDescriptor[]>;
66
+
67
+ // Traversal
68
+ closest(opts: FindClosestOptions, tabId?: string): Promise<ElementDescriptor[]>;
69
+ parent(selector: string, tabId?: string): Promise<ElementDescriptor[]>;
70
+
71
+ // Boolean / numeric
72
+ count(selector: string, tabId?: string): Promise<number>;
73
+ exists(selector: string, tabId?: string): Promise<boolean>;
74
+ visible(selector: string, tabId?: string): Promise<boolean>;
75
+ enabled(selector: string, tabId?: string): Promise<boolean>;
76
+ checked(selector: string, tabId?: string): Promise<boolean>;
77
+ }
78
+
79
+ export declare function createFindAPI(client: PiggyClient): FindClient;
@@ -0,0 +1,165 @@
1
+ // piggy/find/index.ts
2
+ import { PiggyClient } from "../client";
3
+
4
+ // ─── Element descriptor ───────────────────────────────────────────────────────
5
+ // Mirrors __nb_serialize() in PiggyFind.cpp
6
+
7
+ export interface ElementDescriptor {
8
+ tag: string;
9
+ id: string;
10
+ cls: string;
11
+ /** First 400 chars of innerText */
12
+ text: string;
13
+ /** First 800 chars of innerHTML */
14
+ html: string;
15
+ href: string;
16
+ src: string;
17
+ value: string;
18
+ attrs: Record<string, string>;
19
+ }
20
+
21
+ // ─── Option types ─────────────────────────────────────────────────────────────
22
+
23
+ export interface FindByTextOptions {
24
+ text: string;
25
+ /** Narrow the search to descendants of this CSS selector. */
26
+ selector?: string;
27
+ /** If true, innerText must match exactly (trimmed). Default: false. */
28
+ exact?: boolean;
29
+ }
30
+
31
+ export interface FindByAttrOptions {
32
+ attr: string;
33
+ /** If omitted, matches any element that has the attribute at all. */
34
+ value?: string;
35
+ /** Optionally scope to a parent selector. */
36
+ selector?: string;
37
+ }
38
+
39
+ export interface FindByRoleOptions {
40
+ role: string;
41
+ /** Filter by aria-label or innerText containing this string. */
42
+ name?: string;
43
+ }
44
+
45
+ export interface FindClosestOptions {
46
+ /** CSS selector for the starting element. */
47
+ selector: string;
48
+ /** CSS selector for the ancestor to climb to. */
49
+ ancestor: string;
50
+ }
51
+
52
+ export interface FindFilterOptions {
53
+ selector: string;
54
+ attr: string;
55
+ value: string;
56
+ }
57
+
58
+ // ─── FindClient ───────────────────────────────────────────────────────────────
59
+
60
+ export class FindClient {
61
+ constructor(private client: PiggyClient) {}
62
+
63
+ // ── Multi-result queries ─────────────────────────────────────────────────────
64
+
65
+ /** querySelectorAll — returns all matching elements. */
66
+ css(selector: string, tabId = "default"): Promise<ElementDescriptor[]> {
67
+ return this.client.send("find.css", { selector, tabId });
68
+ }
69
+
70
+ /** Alias for css() — querySelectorAll. */
71
+ all(selector: string, tabId = "default"): Promise<ElementDescriptor[]> {
72
+ return this.client.send("find.all", { selector, tabId });
73
+ }
74
+
75
+ /** querySelector — returns a single-element array or []. */
76
+ first(selector: string, tabId = "default"): Promise<ElementDescriptor[]> {
77
+ return this.client.send("find.first", { selector, tabId });
78
+ }
79
+
80
+ /** Find elements whose innerText contains (or exactly matches) the given text. */
81
+ byText(opts: FindByTextOptions, tabId = "default"): Promise<ElementDescriptor[]> {
82
+ return this.client.send("find.byText", { ...opts, tabId });
83
+ }
84
+
85
+ /** Find elements by attribute name and optional value. */
86
+ byAttr(opts: FindByAttrOptions, tabId = "default"): Promise<ElementDescriptor[]> {
87
+ return this.client.send("find.byAttr", { ...opts, tabId });
88
+ }
89
+
90
+ /** getElementsByTagName. */
91
+ byTag(tag: string, tabId = "default"): Promise<ElementDescriptor[]> {
92
+ return this.client.send("find.byTag", { tag, tabId });
93
+ }
94
+
95
+ /** Find inputs/textareas whose placeholder contains the given text. */
96
+ byPlaceholder(text: string, tabId = "default"): Promise<ElementDescriptor[]> {
97
+ return this.client.send("find.byPlaceholder", { text, tabId });
98
+ }
99
+
100
+ /** Find elements by ARIA role, optionally filtered by aria-label / innerText. */
101
+ byRole(opts: FindByRoleOptions, tabId = "default"): Promise<ElementDescriptor[]> {
102
+ return this.client.send("find.byRole", { ...opts, tabId });
103
+ }
104
+
105
+ /** Direct children of the matched element. */
106
+ children(selector: string, tabId = "default"): Promise<ElementDescriptor[]> {
107
+ return this.client.send("find.children", { selector, tabId });
108
+ }
109
+
110
+ /**
111
+ * Filter querySelectorAll results by attribute value substring.
112
+ * Equivalent to: querySelectorAll(selector).filter(el => el.attr.includes(value))
113
+ */
114
+ filter(opts: FindFilterOptions, tabId = "default"): Promise<ElementDescriptor[]> {
115
+ return this.client.send("find.filter", { ...opts, tabId });
116
+ }
117
+
118
+ // ── Single-element traversal ──────────────────────────────────────────────
119
+
120
+ /** Walk up the DOM from selector until ancestor matches. */
121
+ closest(opts: FindClosestOptions, tabId = "default"): Promise<ElementDescriptor[]> {
122
+ return this.client.send("find.closest", { ...opts, tabId });
123
+ }
124
+
125
+ /** parentElement of the matched element. */
126
+ parent(selector: string, tabId = "default"): Promise<ElementDescriptor[]> {
127
+ return this.client.send("find.parent", { selector, tabId });
128
+ }
129
+
130
+ // ── Boolean / numeric queries ─────────────────────────────────────────────
131
+
132
+ /** Number of elements matching the selector. */
133
+ count(selector: string, tabId = "default"): Promise<number> {
134
+ return this.client.send("find.count", { selector, tabId });
135
+ }
136
+
137
+ /** True if at least one element matches the selector. */
138
+ exists(selector: string, tabId = "default"): Promise<boolean> {
139
+ return this.client.send("find.exists", { selector, tabId });
140
+ }
141
+
142
+ /**
143
+ * True if the first matched element is visible
144
+ * (display !== none, visibility !== hidden, opacity !== 0).
145
+ */
146
+ visible(selector: string, tabId = "default"): Promise<boolean> {
147
+ return this.client.send("find.visible", { selector, tabId });
148
+ }
149
+
150
+ /** True if the first matched element is not disabled. */
151
+ enabled(selector: string, tabId = "default"): Promise<boolean> {
152
+ return this.client.send("find.enabled", { selector, tabId });
153
+ }
154
+
155
+ /** True if the first matched checkbox/radio is checked. */
156
+ checked(selector: string, tabId = "default"): Promise<boolean> {
157
+ return this.client.send("find.checked", { selector, tabId });
158
+ }
159
+ }
160
+
161
+ // ── Factory helper ────────────────────────────────────────────────────────────
162
+
163
+ export function createFindAPI(client: PiggyClient): FindClient {
164
+ return new FindClient(client);
165
+ }
@@ -0,0 +1,65 @@
1
+ // piggy/http/index.ts
2
+ // Mirrors PiggyHttp.cpp — connects to the HTTP server on port 2005.
3
+ // Use this when you want to talk to the browser over HTTP instead of the unix socket.
4
+
5
+ export interface HttpClientOptions {
6
+ host?: string;
7
+ port?: number;
8
+ key: string;
9
+ }
10
+
11
+ export class PiggyHttpClient {
12
+ private baseUrl: string;
13
+ private key: string;
14
+
15
+ constructor(opts: HttpClientOptions) {
16
+ const host = opts.host ?? "localhost";
17
+ const port = opts.port ?? 2005;
18
+ this.baseUrl = `http://${host}:${port}`;
19
+ this.key = opts.key;
20
+ }
21
+
22
+ // ── Health check ──────────────────────────────────────────────────────────
23
+
24
+ async ping(): Promise<boolean> {
25
+ try {
26
+ const res = await fetch(this.baseUrl, {
27
+ method: "POST",
28
+ headers: {
29
+ "Content-Type": "application/json",
30
+ "X-Piggy-Key": this.key,
31
+ },
32
+ body: "hello",
33
+ });
34
+ const text = await res.text();
35
+ return text.includes("active");
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ // ── Send command ──────────────────────────────────────────────────────────
42
+
43
+ async send<T = any>(cmd: string, payload: Record<string, any> = {}): Promise<T> {
44
+ const res = await fetch(this.baseUrl, {
45
+ method: "POST",
46
+ headers: {
47
+ "Content-Type": "application/json",
48
+ "X-Piggy-Key": this.key,
49
+ },
50
+ body: JSON.stringify({ id: String(Date.now()), cmd, payload }),
51
+ });
52
+
53
+ if (res.status === 401) throw new Error("Unauthorized — invalid X-Piggy-Key");
54
+ if (res.status === 400) throw new Error("Bad request — invalid JSON");
55
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
56
+
57
+ const data = await res.json() as { ok: boolean; data: T };
58
+ if (!data.ok) throw new Error(String(data.data) ?? "command failed");
59
+ return data.data;
60
+ }
61
+ }
62
+
63
+ export function createHttpClient(opts: HttpClientOptions): PiggyHttpClient {
64
+ return new PiggyHttpClient(opts);
65
+ }
@@ -1,53 +1,115 @@
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
- }
1
+ // piggy/human/index.ts
2
+ import { PiggyClient } from "../client";
3
+
4
+ // ─── Local utilities ──────────────────────────────────────────────────────────
5
+
6
+ export function randomDelay(min: number, max: number): Promise<void> {
7
+ return new Promise(r => setTimeout(r, Math.floor(Math.random() * (max - min + 1)) + min));
8
+ }
9
+
10
+ export function humanTypeSequence(text: string): string[] {
11
+ const adjacent: Record<string, string[]> = {
12
+ a: ["q","w","s","z"], b: ["v","g","h","n"], c: ["x","d","f","v"],
13
+ d: ["s","e","r","f","c","x"], e: ["w","r","d","s"],
14
+ f: ["d","r","t","g","v","c"], g: ["f","t","y","h","b","v"],
15
+ h: ["g","y","u","j","n","b"], i: ["u","o","k","j"],
16
+ j: ["h","u","i","k","m","n"], k: ["j","i","o","l","m"],
17
+ l: ["k","o","p"], m: ["n","j","k"], n: ["b","h","j","m"],
18
+ o: ["i","p","l","k"], p: ["o","l"], q: ["w","a"],
19
+ r: ["e","t","f","d"], s: ["a","w","e","d","x","z"],
20
+ t: ["r","y","g","f"], u: ["y","i","h","j"],
21
+ v: ["c","f","g","b"], w: ["q","e","a","s"],
22
+ x: ["z","s","d","c"], y: ["t","u","g","h"],
23
+ z: ["a","s","x"],
24
+ };
25
+
26
+ const actions: string[] = [];
27
+ const typoIndices = new Set<number>();
28
+
29
+ if (text.length > 4) {
30
+ let tries = 0;
31
+ while (typoIndices.size < 2 && tries < 20) {
32
+ typoIndices.add(Math.floor(Math.random() * text.length));
33
+ tries++;
34
+ }
35
+ }
36
+
37
+ for (let i = 0; i < text.length; i++) {
38
+ if (typoIndices.has(i)) {
39
+ const ch = text[i]!.toLowerCase();
40
+ const neighbors = adjacent[ch];
41
+ const typo = neighbors
42
+ ? neighbors[Math.floor(Math.random() * neighbors.length)] ?? ch
43
+ : ch;
44
+ actions.push(typo);
45
+ actions.push("BACKSPACE");
46
+ }
47
+ actions.push(text[i]!);
48
+ }
49
+
50
+ return actions;
51
+ }
52
+
53
+ // ─── Profile types ────────────────────────────────────────────────────────────
54
+
55
+ export type TypingSpeed = "slow" | "normal" | "fast";
56
+ export type ClickDelay = "cautious" | "normal" | "fast";
57
+ export type ScrollSpeed = "slow" | "normal" | "fast";
58
+
59
+ export interface HumanProfile {
60
+ typingSpeed: TypingSpeed;
61
+ clickDelay: ClickDelay;
62
+ scrollSpeed: ScrollSpeed;
63
+ mouseWiggle: boolean;
64
+ }
65
+
66
+ export interface HumanSetOptions {
67
+ typingSpeed?: TypingSpeed;
68
+ clickDelay?: ClickDelay;
69
+ scrollSpeed?: ScrollSpeed;
70
+ mouseWiggle?: boolean;
71
+ }
72
+
73
+ export interface HumanTypeOptions {
74
+ selector: string;
75
+ text: string;
76
+ clear?: boolean;
77
+ speed?: number;
78
+ }
79
+
80
+ export interface HumanClickOptions {
81
+ selector: string;
82
+ force?: boolean;
83
+ delay?: number;
84
+ }
85
+
86
+ // ─── HumanClient ─────────────────────────────────────────────────────────────
87
+ // Maps 1:1 to PiggyHuman.cpp commands
88
+
89
+ export class HumanClient {
90
+ constructor(private client: PiggyClient) {}
91
+
92
+ // human.set — update global profile
93
+ set(opts: HumanSetOptions, tabId = "default"): Promise<HumanProfile> {
94
+ return this.client.send("human.set", { ...opts, tabId });
95
+ }
96
+
97
+ // human.get — read current profile
98
+ get(tabId = "default"): Promise<HumanProfile> {
99
+ return this.client.send("human.get", { tabId });
100
+ }
101
+
102
+ // human.type — char-by-char typing with realistic delays
103
+ type(opts: HumanTypeOptions, tabId = "default"): Promise<void> {
104
+ return this.client.send("human.type", { ...opts, tabId });
105
+ }
106
+
107
+ // human.click — delayed click with optional force dispatch
108
+ click(opts: HumanClickOptions, tabId = "default"): Promise<void> {
109
+ return this.client.send("human.click", { ...opts, tabId });
110
+ }
111
+ }
112
+
113
+ export function createHumanAPI(client: PiggyClient): HumanClient {
114
+ return new HumanClient(client);
115
+ }