ghostfill 0.1.3 → 0.2.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.
package/README.md CHANGED
@@ -1,8 +1,56 @@
1
1
  # GhostFill
2
2
 
3
- Dev tool that fills form fields with sample data. Select a block, click fill — done.
3
+ Stop demoing with test123. Generate realistic, domain-aware form data in one click.
4
4
 
5
- Works with any framework (React, Vue, Angular, vanilla). Detects inputs, textareas, selects, checkboxes, radios, date pickers, and custom dropdowns (Headless UI, Radix).
5
+ ## The Problem
6
+
7
+ Every team has this story. A PM is demoing to a client, and right there on the screen: "Cheeseburger" in the company name field, "asdf" for the email, and "John Doe" — the same John Doe — on every single record. QA tests with garbage. Designers screenshot forms full of "test123". There's no easy way to fill forms with realistic, context-appropriate data without writing seed scripts or maintaining test fixtures.
8
+
9
+ **Before GhostFill:**
10
+ ```
11
+ First Name: test
12
+ Last Name: test
13
+ Email: test@test.com
14
+ Company: asdfasdf
15
+ Job Title: fjdksl
16
+ Phone: 1234567890
17
+ Address: 123 test street
18
+ ```
19
+
20
+ **After GhostFill:**
21
+ ```
22
+ First Name: Sarah
23
+ Last Name: Mitchell
24
+ Email: s.mitchell@northwind.com
25
+ Company: Northwind Traders
26
+ Job Title: Senior Account Executive
27
+ Phone: +1 (312) 555-0187
28
+ Address: 401 N Michigan Ave, Chicago, IL 60611
29
+ ```
30
+
31
+ One click. Any form. No seed scripts.
32
+
33
+ ## Who It's For
34
+
35
+ **Developers** install it — `npm install ghostfill -D`, one import, done. But the whole team benefits:
36
+
37
+ - **QA Engineers** — fill forms with realistic edge cases instead of copy-pasting the same test data
38
+ - **Product Managers** — demo to clients with professional-looking data, not "test123"
39
+ - **Designers** — screenshot real-looking forms for decks and specs
40
+ - **Solution Architects** — show realistic D365, Salesforce, or ERP data during workshops
41
+
42
+ ## Not Another Faker
43
+
44
+ faker.js fills your database. Browser autofill fills *your* data. GhostFill fills the form you're looking at — visually, with data that fits the context.
45
+
46
+ | | GhostFill | faker.js | Browser Autofill |
47
+ |---|---|---|---|
48
+ | **Who uses it** | Anyone on the team | Developers only | Individual user |
49
+ | **How it works** | Visual in-page UI | Code library | Browser feature |
50
+ | **Data quality** | Domain-aware, contextual | Random but typed | Your personal data |
51
+ | **Form detection** | Reads labels, selects, custom dropdowns | N/A — you call it in code | Standard inputs only |
52
+ | **Domain presets** | D365, Healthcare, E-commerce, etc. | Generic schemas | N/A |
53
+ | **Setup** | One import, zero config | Write generation scripts | Already there |
6
54
 
7
55
  ## Install
8
56
 
@@ -28,7 +76,14 @@ import { useEffect } from "react";
28
76
 
29
77
  function GhostFill() {
30
78
  useEffect(() => {
31
- import("ghostfill").then((m) => m.init());
79
+ import("ghostfill").then((m) =>
80
+ m.init({
81
+ ai: {
82
+ endpoint: "/api/ghostfill",
83
+ provider: "openai",
84
+ },
85
+ })
86
+ );
32
87
  }, []);
33
88
  return null;
34
89
  }
@@ -51,22 +106,106 @@ await fill({ container: document.querySelector("form") });
51
106
  2. Click it to enter selection mode — hover and click a form area
52
107
  3. Click the sparkles button to fill all detected fields
53
108
  4. By default, generates random sample data locally (no API needed)
54
- 5. Optionally enable AI mode in settings for context-aware data generation
109
+ 5. Optionally enable AI mode in settings for context-aware data generation through your backend
110
+
111
+ ## Presets
112
+
113
+ Save prompt templates for domain-specific data:
114
+
115
+ - **D365 CE** — Accounts, Contacts, Opportunities with CRM-realistic values
116
+ - **Healthcare** — Patient intake, insurance, clinical forms
117
+ - **E-commerce** — Products, orders, customer profiles
118
+ - **Automotive** — Vehicle specs, service records, dealer info
119
+ - **Custom** — Write your own prompt template for any domain
120
+
121
+ Presets are stored locally in the browser and sent as context to the AI provider.
122
+
123
+ ## Secure AI Setup
124
+
125
+ GhostFill no longer accepts provider API keys in the browser. To use OpenAI, xAI, or Moonshot safely, expose a backend route and keep provider credentials server-side.
126
+
127
+ Install a server-side SDK in your app:
128
+
129
+ ```bash
130
+ npm install openai
131
+ ```
132
+
133
+ Example Next.js route:
134
+
135
+ ```ts
136
+ // app/api/ghostfill/route.ts
137
+ import OpenAI from "openai";
138
+ import {
139
+ buildFillMessages,
140
+ parseFillDataPayload,
141
+ type GhostFillAIRequest,
142
+ type Provider,
143
+ } from "ghostfill/server";
144
+
145
+ const clients: Record<Provider, OpenAI> = {
146
+ openai: new OpenAI({
147
+ apiKey: process.env.OPENAI_API_KEY,
148
+ }),
149
+ xai: new OpenAI({
150
+ apiKey: process.env.XAI_API_KEY,
151
+ baseURL: "https://api.x.ai/v1",
152
+ }),
153
+ moonshot: new OpenAI({
154
+ apiKey: process.env.MOONSHOT_API_KEY,
155
+ baseURL: "https://api.moonshot.cn/v1",
156
+ }),
157
+ };
158
+
159
+ const models: Record<Provider, string> = {
160
+ openai: process.env.GHOSTFILL_OPENAI_MODEL || "gpt-4o-mini",
161
+ xai: process.env.GHOSTFILL_XAI_MODEL || "grok-4-fast",
162
+ moonshot: process.env.GHOSTFILL_MOONSHOT_MODEL || "moonshot-v1-8k",
163
+ };
164
+
165
+ function isProvider(value: unknown): value is Provider {
166
+ return value === "openai" || value === "xai" || value === "moonshot";
167
+ }
168
+
169
+ export async function POST(req: Request) {
170
+ const body = (await req.json()) as Partial<GhostFillAIRequest>;
171
+
172
+ if (!isProvider(body.provider) || !Array.isArray(body.fields)) {
173
+ return Response.json({ error: "Invalid GhostFill request" }, { status: 400 });
174
+ }
175
+
176
+ const completion = await clients[body.provider].chat.completions.create({
177
+ model: models[body.provider],
178
+ messages: buildFillMessages({
179
+ provider: body.provider,
180
+ prompt: typeof body.prompt === "string" ? body.prompt : "",
181
+ systemPrompt:
182
+ typeof body.systemPrompt === "string" ? body.systemPrompt : undefined,
183
+ fields: body.fields,
184
+ }),
185
+ });
186
+
187
+ const content = completion.choices[0]?.message?.content || "";
188
+ return Response.json(parseFillDataPayload(content));
189
+ }
190
+ ```
191
+
192
+ This route supports all three providers, keeps secrets server-side, and only sends non-secret field metadata to the model. If you expose it outside local development, add your app's auth, rate limits, and request-size validation.
55
193
 
56
194
  ## Settings
57
195
 
58
196
  Click the gear icon to configure:
59
197
 
60
198
  - **Highlight Colour** — pick the selection overlay color
61
- - **Use AI** — toggle AI-powered fills (requires API key)
62
- - **Provider** — cycle between OpenAI (gpt-4o-mini), xAI (Grok 4 Fast), Moonshot (Kimi K2)
63
- - **API Key** — your provider API key
64
- - **Presets** — save prompt templates for domain-specific data (e.g. D365, healthcare)
199
+ - **Use AI** — toggle AI-powered fills when a secure backend route is configured
200
+ - **Provider** — cycle between OpenAI, xAI, and Moonshot
201
+ - **Backend** — shows the secure route or handler being used for AI fills
202
+ - **Presets** — save prompt templates for domain-specific data (e.g. D365, healthcare); keep them non-secret because they are stored locally
65
203
  - **Dark/Light theme** — toggle with the sun/moon icon
66
204
 
67
205
  ## Features
68
206
 
69
207
  - **Zero config** — works out of the box with random sample data
208
+ - **Secure by default** — AI mode uses a backend route instead of browser-held provider keys
70
209
  - **Shadow DOM** — styles don't leak into your app
71
210
  - **Framework-aware** — uses native value setters so React/Vue/Angular pick up changes
72
211
  - **Smart detection** — labels from `<label>`, `aria-label`, placeholder, preceding siblings
@@ -84,9 +223,13 @@ Initialize GhostFill and add the UI to the page.
84
223
 
85
224
  ```ts
86
225
  init({
87
- apiKey?: string, // API key (can also set in UI)
88
- shortcut?: string, // Keyboard shortcut (default: "Alt+G")
89
- systemPrompt?: string // Custom system prompt to prepend
226
+ ai?: {
227
+ endpoint?: string, // Same-origin backend route (default: "/api/ghostfill")
228
+ requestFillData?: (request: GhostFillAIRequest) => Promise<FieldFillData[]>,
229
+ provider?: "openai" | "xai" | "moonshot"
230
+ },
231
+ shortcut?: string, // Keyboard shortcut (default: "Alt+G")
232
+ systemPrompt?: string // Custom system prompt to prepend
90
233
  })
91
234
  ```
92
235
 
@@ -99,7 +242,14 @@ Programmatic fill without the UI.
99
242
  ```ts
100
243
  await fill({
101
244
  container: HTMLElement, // The element containing form fields
102
- prompt?: string // Optional prompt for AI mode
245
+ prompt?: string, // Optional prompt for AI mode
246
+ ai?: {
247
+ endpoint?: string,
248
+ requestFillData?: (request: GhostFillAIRequest) => Promise<FieldFillData[]>,
249
+ provider?: "openai" | "xai" | "moonshot"
250
+ },
251
+ provider?: "openai" | "xai" | "moonshot",
252
+ systemPrompt?: string
103
253
  })
104
254
  ```
105
255
 
@@ -0,0 +1,275 @@
1
+ // src/types.ts
2
+ var PROVIDERS = {
3
+ openai: { label: "OpenAI", model: "gpt-4o-mini", baseURL: "https://api.openai.com/v1", helpText: "Uses gpt-4o-mini \u2014 fast & cheap" },
4
+ xai: { label: "xAI", model: "grok-4-fast", baseURL: "https://api.x.ai/v1", helpText: "Uses Grok 4 Fast" },
5
+ moonshot: { label: "Moonshot", model: "kimi-k2", baseURL: "https://api.moonshot.ai/v1", helpText: "Uses Kimi K2 \u2014 fast & cheap" }
6
+ };
7
+
8
+ // src/detector.ts
9
+ var INPUT_SELECTORS = [
10
+ "input:not([type=hidden]):not([type=submit]):not([type=button]):not([type=reset]):not([type=image])",
11
+ "textarea",
12
+ "select",
13
+ // Custom dropdown triggers (Headless UI Listbox, Radix, etc.)
14
+ "button[role=combobox]",
15
+ "[role=combobox]:not(input)",
16
+ "button[aria-haspopup=listbox]"
17
+ ].join(", ");
18
+ function findLabel(el) {
19
+ if (el.id) {
20
+ const label = document.querySelector(
21
+ `label[for="${CSS.escape(el.id)}"]`
22
+ );
23
+ if (label?.textContent?.trim()) return label.textContent.trim();
24
+ }
25
+ const parentLabel = el.closest("label");
26
+ if (parentLabel) {
27
+ const clone = parentLabel.cloneNode(true);
28
+ clone.querySelectorAll("input, textarea, select").forEach((c) => c.remove());
29
+ const text = clone.textContent?.trim();
30
+ if (text) return text;
31
+ }
32
+ const ariaLabel = el.getAttribute("aria-label");
33
+ if (ariaLabel?.trim()) return ariaLabel.trim();
34
+ const labelledBy = el.getAttribute("aria-labelledby");
35
+ if (labelledBy) {
36
+ const parts = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean);
37
+ if (parts.length) return parts.join(" ");
38
+ }
39
+ if ("placeholder" in el) {
40
+ const ph = el.placeholder;
41
+ if (ph?.trim()) return ph.trim();
42
+ }
43
+ const title = el.getAttribute("title");
44
+ if (title?.trim()) return title.trim();
45
+ const prev = el.previousElementSibling;
46
+ if (prev && !["INPUT", "TEXTAREA", "SELECT"].includes(prev.tagName)) {
47
+ const prevText = prev.textContent?.trim();
48
+ if (prevText && prevText.length < 60) return prevText;
49
+ }
50
+ const parent = el.parentElement;
51
+ if (parent) {
52
+ for (const child of Array.from(parent.children)) {
53
+ if (child === el) break;
54
+ if (["LABEL", "SPAN", "P", "H1", "H2", "H3", "H4", "H5", "H6", "LEGEND", "DIV"].includes(child.tagName)) {
55
+ const text = child.textContent?.trim();
56
+ if (text && text.length < 60 && !child.querySelector("input, textarea, select")) {
57
+ return text;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ if (el.id) return el.id.replace(/[_\-]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim();
63
+ const name = el.getAttribute("name");
64
+ if (name) return name.replace(/[_\-[\]]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim();
65
+ return "unknown";
66
+ }
67
+ function detectFields(container) {
68
+ const elements = container.querySelectorAll(INPUT_SELECTORS);
69
+ const fields = [];
70
+ elements.forEach((el) => {
71
+ if (el.disabled) return;
72
+ if (el.readOnly) return;
73
+ if (el.offsetParent === null && el.type !== "hidden") return;
74
+ const isCustomDropdown = el.getAttribute("role") === "combobox" && !(el instanceof HTMLInputElement) || el.getAttribute("aria-haspopup") === "listbox";
75
+ if (isCustomDropdown) {
76
+ const listboxId = el.getAttribute("aria-controls");
77
+ let listbox = listboxId ? document.getElementById(listboxId) : null;
78
+ if (!listbox) {
79
+ listbox = el.parentElement?.querySelector("[role=listbox]") || null;
80
+ }
81
+ const options = [];
82
+ if (listbox) {
83
+ listbox.querySelectorAll("[role=option]").forEach((opt) => {
84
+ const text = opt.textContent?.trim();
85
+ if (text) options.push(text);
86
+ });
87
+ }
88
+ const buttonText = el.textContent?.trim() || "";
89
+ const looksLikePlaceholder = buttonText.toLowerCase().startsWith("select") || buttonText === "";
90
+ const field2 = {
91
+ element: el,
92
+ type: "select",
93
+ name: el.id || el.getAttribute("name") || "",
94
+ label: findLabel(el),
95
+ required: el.getAttribute("aria-required") === "true",
96
+ currentValue: looksLikePlaceholder ? "" : buttonText,
97
+ options: options.length > 0 ? options : void 0
98
+ };
99
+ fields.push(field2);
100
+ return;
101
+ }
102
+ const field = {
103
+ element: el,
104
+ type: el instanceof HTMLSelectElement ? "select" : el.type || "text",
105
+ name: el.name || el.id || "",
106
+ label: findLabel(el),
107
+ required: el.required || el.getAttribute("aria-required") === "true",
108
+ currentValue: el.value
109
+ };
110
+ if (el instanceof HTMLSelectElement) {
111
+ field.options = Array.from(el.options).filter((opt) => opt.value && !opt.disabled).map((opt) => opt.textContent?.trim() || opt.value);
112
+ }
113
+ if ("min" in el && el.min) {
114
+ field.min = el.min;
115
+ }
116
+ if ("max" in el && el.max) {
117
+ field.max = el.max;
118
+ }
119
+ if ("pattern" in el && el.pattern) {
120
+ field.pattern = el.pattern;
121
+ }
122
+ fields.push(field);
123
+ });
124
+ return fields;
125
+ }
126
+ function describeFields(fields) {
127
+ return fields.map((f, i) => {
128
+ let desc = `[${i}] "${f.label}" (type: ${f.type}`;
129
+ if (f.required) desc += ", required";
130
+ if (f.options?.length) desc += `, options: [${f.options.join(", ")}]`;
131
+ if (f.min) desc += `, min: ${f.min}`;
132
+ if (f.max) desc += `, max: ${f.max}`;
133
+ if (f.pattern) desc += `, pattern: ${f.pattern}`;
134
+ desc += ")";
135
+ return desc;
136
+ }).join("\n");
137
+ }
138
+
139
+ // src/ai.ts
140
+ var SYSTEM_PROMPT = `You are a form-filling assistant. Given a list of form fields and an optional user prompt, generate realistic fake data to fill ALL fields.
141
+
142
+ Rules:
143
+ - Return ONLY a JSON object with a "fields" array of objects, each with "index" and "value" keys
144
+ - You MUST fill EVERY field \u2014 do not skip any
145
+ - Match the field type (email \u2192 valid email, phone \u2192 valid phone with country code, date \u2192 YYYY-MM-DD, datetime-local \u2192 YYYY-MM-DDTHH:MM, etc.)
146
+ - For select/dropdown fields: you MUST pick one of the listed options EXACTLY as written
147
+ - For checkboxes: add a "checked" boolean (true or false)
148
+ - For radio buttons: only fill one per group, use the option value
149
+ - Respect min/max constraints and patterns
150
+ - Generate contextually coherent data (same person's name, matching city/state/zip, etc.)
151
+ - If no user prompt is given, infer appropriate data from the field labels
152
+ - Do NOT request or rely on secrets, tokens, passwords, or existing form values
153
+ - Do NOT wrap the JSON in markdown code blocks \u2014 return raw JSON only`;
154
+ function isRecord(value) {
155
+ return typeof value === "object" && value !== null;
156
+ }
157
+ function toFieldFillData(value) {
158
+ if (!isRecord(value)) {
159
+ throw new Error("AI response item is not an object");
160
+ }
161
+ const index = value.index;
162
+ const rawValue = value.value;
163
+ const checked = value.checked;
164
+ if (typeof index !== "number" || !Number.isInteger(index)) {
165
+ throw new Error("AI response item is missing a numeric index");
166
+ }
167
+ if (typeof rawValue !== "string") {
168
+ throw new Error("AI response item is missing a string value");
169
+ }
170
+ return {
171
+ index,
172
+ value: rawValue,
173
+ checked: typeof checked === "boolean" ? checked : void 0
174
+ };
175
+ }
176
+ function toPromptFields(fields) {
177
+ return fields.map((field, index) => ({
178
+ index,
179
+ type: field.type,
180
+ name: field.name,
181
+ label: field.label,
182
+ options: field.options,
183
+ required: field.required,
184
+ min: field.min,
185
+ max: field.max,
186
+ pattern: field.pattern
187
+ }));
188
+ }
189
+ function buildFillMessages(request) {
190
+ const fieldDescription = describeFields(request.fields);
191
+ let userContent = `Form fields:
192
+ ${fieldDescription}`;
193
+ if (request.prompt) {
194
+ userContent += `
195
+
196
+ User instructions: ${request.prompt}`;
197
+ } else {
198
+ userContent += "\n\nNo specific instructions \u2014 generate realistic, contextually appropriate data for all fields.";
199
+ }
200
+ return [
201
+ {
202
+ role: "system",
203
+ content: request.systemPrompt ? `${request.systemPrompt}
204
+
205
+ ${SYSTEM_PROMPT}` : SYSTEM_PROMPT
206
+ },
207
+ {
208
+ role: "user",
209
+ content: userContent
210
+ }
211
+ ];
212
+ }
213
+ function parseFillDataPayload(payload) {
214
+ if (typeof payload === "string") {
215
+ const jsonMatch = payload.match(/```(?:json)?\s*([\s\S]*?)```/) || [null, payload];
216
+ return parseFillDataPayload(JSON.parse(jsonMatch[1].trim()));
217
+ }
218
+ if (Array.isArray(payload)) {
219
+ return payload.map(toFieldFillData);
220
+ }
221
+ if (isRecord(payload)) {
222
+ if (Array.isArray(payload.choices)) {
223
+ const content = payload.choices[0]?.message;
224
+ if (typeof content?.content === "string") {
225
+ return parseFillDataPayload(content.content);
226
+ }
227
+ }
228
+ const candidate = payload.fields ?? payload.data ?? payload.items ?? payload.result;
229
+ if (Array.isArray(candidate)) {
230
+ return candidate.map(toFieldFillData);
231
+ }
232
+ }
233
+ throw new Error("AI response is not an array of field fills");
234
+ }
235
+ function createRequest(fields, userPrompt, provider, systemPrompt) {
236
+ return {
237
+ provider,
238
+ prompt: userPrompt,
239
+ systemPrompt,
240
+ fields: toPromptFields(fields)
241
+ };
242
+ }
243
+ async function generateFillData(fields, userPrompt, provider, transport, systemPrompt) {
244
+ const request = createRequest(fields, userPrompt, provider, systemPrompt);
245
+ if (transport.requestFillData) {
246
+ return transport.requestFillData(request);
247
+ }
248
+ const endpoint = transport.endpoint ?? "/api/ghostfill";
249
+ const response = await fetch(endpoint, {
250
+ method: "POST",
251
+ headers: {
252
+ "Content-Type": "application/json"
253
+ },
254
+ body: JSON.stringify(request)
255
+ });
256
+ if (!response.ok) {
257
+ const error = await response.text();
258
+ throw new Error(`AI API error (${response.status}): ${error}`);
259
+ }
260
+ const contentType = response.headers.get("content-type") || "";
261
+ const payload = contentType.includes("application/json") ? await response.json() : await response.text();
262
+ return parseFillDataPayload(payload);
263
+ }
264
+
265
+ export {
266
+ PROVIDERS,
267
+ detectFields,
268
+ describeFields,
269
+ SYSTEM_PROMPT,
270
+ toPromptFields,
271
+ buildFillMessages,
272
+ parseFillDataPayload,
273
+ generateFillData
274
+ };
275
+ //# sourceMappingURL=chunk-VMCU3BNJ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/detector.ts","../src/ai.ts"],"sourcesContent":["/** Configuration options for GhostFill */\nexport interface GhostFillOptions {\n /**\n * @deprecated Browser API keys are insecure and ignored.\n * Configure `ai` and keep provider credentials on your backend instead.\n */\n apiKey?: string;\n /** Keyboard shortcut to toggle GhostFill (default: \"Alt+G\") */\n shortcut?: string;\n /** Custom system prompt to prepend */\n systemPrompt?: string;\n /** Secure AI transport configuration */\n ai?: GhostFillAIOptions;\n}\n\nexport type Provider = \"openai\" | \"xai\" | \"moonshot\";\n\nexport const PROVIDERS: Record<Provider, { label: string; model: string; baseURL: string; helpText: string }> = {\n openai: { label: \"OpenAI\", model: \"gpt-4o-mini\", baseURL: \"https://api.openai.com/v1\", helpText: \"Uses gpt-4o-mini — fast & cheap\" },\n xai: { label: \"xAI\", model: \"grok-4-fast\", baseURL: \"https://api.x.ai/v1\", helpText: \"Uses Grok 4 Fast\" },\n moonshot: { label: \"Moonshot\", model: \"kimi-k2\", baseURL: \"https://api.moonshot.ai/v1\", helpText: \"Uses Kimi K2 — fast & cheap\" },\n};\n\n/** Non-secret field metadata sent to an AI backend */\nexport interface GhostFillPromptField {\n index: number;\n type: string;\n name: string;\n label: string;\n options?: string[];\n required: boolean;\n min?: string;\n max?: string;\n pattern?: string;\n}\n\n/** Secure AI request payload for a backend route or callback */\nexport interface GhostFillAIRequest {\n provider: Provider;\n prompt: string;\n systemPrompt?: string;\n fields: GhostFillPromptField[];\n}\n\nexport type GhostFillAIHandler = (\n request: GhostFillAIRequest\n) => Promise<FieldFillData[]>;\n\n/** Secure AI configuration. Requests must go through a backend or custom handler. */\nexport interface GhostFillAIOptions {\n /** Same-origin backend route. Defaults to `/api/ghostfill` when `ai` is enabled. */\n endpoint?: string;\n /** Optional custom transport that forwards requests to a secure backend. */\n requestFillData?: GhostFillAIHandler;\n /** Default provider shown in the UI. */\n provider?: Provider;\n}\n\n/** A saved prompt preset */\nexport interface Preset {\n id: string;\n name: string;\n prompt: string;\n}\n\n/** Persisted settings (localStorage) */\nexport interface GhostFillSettings {\n apiKey: string;\n provider: Provider;\n highlightColor: string;\n theme: \"dark\" | \"light\";\n useAI: boolean;\n presets: Preset[];\n activePresetId: string | null;\n}\n\n/** A detected form field */\nexport interface DetectedField {\n /** The DOM element */\n element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;\n /** Field type: text, email, number, select, textarea, checkbox, radio, date, etc. */\n type: string;\n /** Field name attribute */\n name: string;\n /** Field label (from <label>, aria-label, or placeholder) */\n label: string;\n /** For <select>, the available options */\n options?: string[];\n /** Whether the field is required */\n required: boolean;\n /** Current value */\n currentValue: string;\n /** Min/max for number/date fields */\n min?: string;\n max?: string;\n /** Pattern attribute */\n pattern?: string;\n}\n\n/** Field data returned by the AI */\nexport interface FieldFillData {\n /** Index matching the DetectedField array */\n index: number;\n /** Value to fill */\n value: string;\n /** For checkboxes: whether to check */\n checked?: boolean;\n}\n\n/** Internal state */\nexport interface GhostFillState {\n active: boolean;\n selecting: boolean;\n selectedBlock: HTMLElement | null;\n fields: DetectedField[];\n overlay: HTMLElement | null;\n shadowRoot: ShadowRoot | null;\n}\n","import type { DetectedField, GhostFillPromptField } from \"./types\";\n\nconst INPUT_SELECTORS = [\n \"input:not([type=hidden]):not([type=submit]):not([type=button]):not([type=reset]):not([type=image])\",\n \"textarea\",\n \"select\",\n // Custom dropdown triggers (Headless UI Listbox, Radix, etc.)\n \"button[role=combobox]\",\n \"[role=combobox]:not(input)\",\n \"button[aria-haspopup=listbox]\",\n].join(\", \");\n\n/** Find the label text for a form field */\nfunction findLabel(el: HTMLElement): string {\n // 1. Explicit <label for=\"id\">\n if (el.id) {\n const label = document.querySelector<HTMLLabelElement>(\n `label[for=\"${CSS.escape(el.id)}\"]`\n );\n if (label?.textContent?.trim()) return label.textContent.trim();\n }\n\n // 2. Wrapping <label>\n const parentLabel = el.closest(\"label\");\n if (parentLabel) {\n // Get text content excluding the input itself\n const clone = parentLabel.cloneNode(true) as HTMLElement;\n clone.querySelectorAll(\"input, textarea, select\").forEach((c) => c.remove());\n const text = clone.textContent?.trim();\n if (text) return text;\n }\n\n // 3. aria-label\n const ariaLabel = el.getAttribute(\"aria-label\");\n if (ariaLabel?.trim()) return ariaLabel.trim();\n\n // 4. aria-labelledby\n const labelledBy = el.getAttribute(\"aria-labelledby\");\n if (labelledBy) {\n const parts = labelledBy\n .split(/\\s+/)\n .map((id) => document.getElementById(id)?.textContent?.trim())\n .filter(Boolean);\n if (parts.length) return parts.join(\" \");\n }\n\n // 5. placeholder\n if (\"placeholder\" in el) {\n const ph = (el as HTMLInputElement).placeholder;\n if (ph?.trim()) return ph.trim();\n }\n\n // 6. title\n const title = el.getAttribute(\"title\");\n if (title?.trim()) return title.trim();\n\n // 7. Preceding sibling text (common pattern: <span>Label</span><input>)\n const prev = el.previousElementSibling;\n if (prev && ![\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(prev.tagName)) {\n const prevText = prev.textContent?.trim();\n if (prevText && prevText.length < 60) return prevText;\n }\n\n // 8. Parent's first text node or heading\n const parent = el.parentElement;\n if (parent) {\n // Look for a heading or label-like element before this input in the parent\n for (const child of Array.from(parent.children)) {\n if (child === el) break;\n if ([\"LABEL\", \"SPAN\", \"P\", \"H1\", \"H2\", \"H3\", \"H4\", \"H5\", \"H6\", \"LEGEND\", \"DIV\"].includes(child.tagName)) {\n const text = child.textContent?.trim();\n if (text && text.length < 60 && !child.querySelector(\"input, textarea, select\")) {\n return text;\n }\n }\n }\n }\n\n // 9. id attribute humanized\n if (el.id) return el.id.replace(/[_\\-]/g, \" \").replace(/([a-z])([A-Z])/g, \"$1 $2\").trim();\n\n // 10. name attribute as fallback\n const name = el.getAttribute(\"name\");\n if (name) return name.replace(/[_\\-[\\]]/g, \" \").replace(/([a-z])([A-Z])/g, \"$1 $2\").trim();\n\n return \"unknown\";\n}\n\n/** Detect all fillable fields within a container element */\nexport function detectFields(container: HTMLElement): DetectedField[] {\n const elements = container.querySelectorAll<\n HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement\n >(INPUT_SELECTORS);\n\n const fields: DetectedField[] = [];\n\n elements.forEach((el) => {\n // Skip disabled fields\n if ((el as HTMLInputElement).disabled) return;\n if ((el as HTMLInputElement).readOnly) return;\n // Skip hidden elements\n if (el.offsetParent === null && (el as HTMLInputElement).type !== \"hidden\") return;\n\n // Custom combobox / listbox (Headless UI, Radix, etc.)\n const isCustomDropdown =\n (el.getAttribute(\"role\") === \"combobox\" && !(el instanceof HTMLInputElement)) ||\n el.getAttribute(\"aria-haspopup\") === \"listbox\";\n if (isCustomDropdown) {\n // Find listbox options: aria-controls, sibling [role=listbox], or Headless UI pattern\n const listboxId = el.getAttribute(\"aria-controls\");\n let listbox: Element | null = listboxId ? document.getElementById(listboxId) : null;\n if (!listbox) {\n // Headless UI puts the listbox as a sibling inside the same relative container\n listbox = el.parentElement?.querySelector(\"[role=listbox]\") || null;\n }\n\n const options: string[] = [];\n if (listbox) {\n listbox.querySelectorAll(\"[role=option]\").forEach((opt) => {\n const text = opt.textContent?.trim();\n if (text) options.push(text);\n });\n }\n\n // Get current display value from button text\n const buttonText = el.textContent?.trim() || \"\";\n // Check if it looks like a placeholder\n const looksLikePlaceholder = buttonText.toLowerCase().startsWith(\"select\") || buttonText === \"\";\n\n const field: DetectedField = {\n element: el as any,\n type: \"select\",\n name: el.id || el.getAttribute(\"name\") || \"\",\n label: findLabel(el),\n required: el.getAttribute(\"aria-required\") === \"true\",\n currentValue: looksLikePlaceholder ? \"\" : buttonText,\n options: options.length > 0 ? options : undefined,\n };\n fields.push(field);\n return;\n }\n\n const field: DetectedField = {\n element: el as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,\n type: el instanceof HTMLSelectElement ? \"select\" : (el as HTMLInputElement).type || \"text\",\n name: (el as HTMLInputElement).name || el.id || \"\",\n label: findLabel(el),\n required: (el as HTMLInputElement).required || el.getAttribute(\"aria-required\") === \"true\",\n currentValue: (el as HTMLInputElement).value,\n };\n\n // Collect select options\n if (el instanceof HTMLSelectElement) {\n field.options = Array.from(el.options)\n .filter((opt) => opt.value && !opt.disabled)\n .map((opt) => opt.textContent?.trim() || opt.value);\n }\n\n // Collect constraints\n if (\"min\" in el && (el as HTMLInputElement).min) {\n field.min = (el as HTMLInputElement).min;\n }\n if (\"max\" in el && (el as HTMLInputElement).max) {\n field.max = (el as HTMLInputElement).max;\n }\n if (\"pattern\" in el && (el as HTMLInputElement).pattern) {\n field.pattern = (el as HTMLInputElement).pattern;\n }\n\n fields.push(field);\n });\n\n return fields;\n}\n\n/** Build a description of fields for the AI prompt */\nexport function describeFields(\n fields: Array<\n Pick<\n DetectedField | GhostFillPromptField,\n \"type\" | \"label\" | \"required\" | \"options\" | \"min\" | \"max\" | \"pattern\"\n >\n >\n): string {\n return fields\n .map((f, i) => {\n let desc = `[${i}] \"${f.label}\" (type: ${f.type}`;\n if (f.required) desc += \", required\";\n if (f.options?.length) desc += `, options: [${f.options.join(\", \")}]`;\n if (f.min) desc += `, min: ${f.min}`;\n if (f.max) desc += `, max: ${f.max}`;\n if (f.pattern) desc += `, pattern: ${f.pattern}`;\n desc += \")\";\n return desc;\n })\n .join(\"\\n\");\n}\n","import { describeFields } from \"./detector\";\nimport type {\n DetectedField,\n FieldFillData,\n GhostFillAIOptions,\n GhostFillAIRequest,\n GhostFillPromptField,\n Provider,\n} from \"./types\";\n\nexport const SYSTEM_PROMPT = `You are a form-filling assistant. Given a list of form fields and an optional user prompt, generate realistic fake data to fill ALL fields.\n\nRules:\n- Return ONLY a JSON object with a \"fields\" array of objects, each with \"index\" and \"value\" keys\n- You MUST fill EVERY field — do not skip any\n- Match the field type (email → valid email, phone → valid phone with country code, date → YYYY-MM-DD, datetime-local → YYYY-MM-DDTHH:MM, etc.)\n- For select/dropdown fields: you MUST pick one of the listed options EXACTLY as written\n- For checkboxes: add a \"checked\" boolean (true or false)\n- For radio buttons: only fill one per group, use the option value\n- Respect min/max constraints and patterns\n- Generate contextually coherent data (same person's name, matching city/state/zip, etc.)\n- If no user prompt is given, infer appropriate data from the field labels\n- Do NOT request or rely on secrets, tokens, passwords, or existing form values\n- Do NOT wrap the JSON in markdown code blocks — return raw JSON only`;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction toFieldFillData(value: unknown): FieldFillData {\n if (!isRecord(value)) {\n throw new Error(\"AI response item is not an object\");\n }\n\n const index = value.index;\n const rawValue = value.value;\n const checked = value.checked;\n\n if (typeof index !== \"number\" || !Number.isInteger(index)) {\n throw new Error(\"AI response item is missing a numeric index\");\n }\n\n if (typeof rawValue !== \"string\") {\n throw new Error(\"AI response item is missing a string value\");\n }\n\n return {\n index,\n value: rawValue,\n checked: typeof checked === \"boolean\" ? checked : undefined,\n };\n}\n\nexport function toPromptFields(fields: DetectedField[]): GhostFillPromptField[] {\n return fields.map((field, index) => ({\n index,\n type: field.type,\n name: field.name,\n label: field.label,\n options: field.options,\n required: field.required,\n min: field.min,\n max: field.max,\n pattern: field.pattern,\n }));\n}\n\nexport function buildFillMessages(\n request: GhostFillAIRequest\n): Array<{ role: \"system\" | \"user\"; content: string }> {\n const fieldDescription = describeFields(request.fields);\n let userContent = `Form fields:\\n${fieldDescription}`;\n\n if (request.prompt) {\n userContent += `\\n\\nUser instructions: ${request.prompt}`;\n } else {\n userContent +=\n \"\\n\\nNo specific instructions — generate realistic, contextually appropriate data for all fields.\";\n }\n\n return [\n {\n role: \"system\",\n content: request.systemPrompt\n ? `${request.systemPrompt}\\n\\n${SYSTEM_PROMPT}`\n : SYSTEM_PROMPT,\n },\n {\n role: \"user\",\n content: userContent,\n },\n ];\n}\n\nexport function parseFillDataPayload(payload: unknown): FieldFillData[] {\n if (typeof payload === \"string\") {\n const jsonMatch =\n payload.match(/```(?:json)?\\s*([\\s\\S]*?)```/) || [null, payload];\n return parseFillDataPayload(JSON.parse(jsonMatch[1]!.trim()));\n }\n\n if (Array.isArray(payload)) {\n return payload.map(toFieldFillData);\n }\n\n if (isRecord(payload)) {\n if (Array.isArray(payload.choices)) {\n const content = (payload.choices[0] as Record<string, unknown> | undefined)\n ?.message as Record<string, unknown> | undefined;\n if (typeof content?.content === \"string\") {\n return parseFillDataPayload(content.content);\n }\n }\n\n const candidate =\n payload.fields ?? payload.data ?? payload.items ?? payload.result;\n if (Array.isArray(candidate)) {\n return candidate.map(toFieldFillData);\n }\n }\n\n throw new Error(\"AI response is not an array of field fills\");\n}\n\nfunction createRequest(\n fields: DetectedField[],\n userPrompt: string,\n provider: Provider,\n systemPrompt?: string\n): GhostFillAIRequest {\n return {\n provider,\n prompt: userPrompt,\n systemPrompt,\n fields: toPromptFields(fields),\n };\n}\n\n/** Call a secure backend or callback to generate fill data. */\nexport async function generateFillData(\n fields: DetectedField[],\n userPrompt: string,\n provider: Provider,\n transport: GhostFillAIOptions,\n systemPrompt?: string\n): Promise<FieldFillData[]> {\n const request = createRequest(fields, userPrompt, provider, systemPrompt);\n\n if (transport.requestFillData) {\n return transport.requestFillData(request);\n }\n\n const endpoint = transport.endpoint ?? \"/api/ghostfill\";\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(request),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`AI API error (${response.status}): ${error}`);\n }\n\n const contentType = response.headers.get(\"content-type\") || \"\";\n const payload = contentType.includes(\"application/json\")\n ? await response.json()\n : await response.text();\n\n return parseFillDataPayload(payload);\n}\n"],"mappings":"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","names":["field"]}
package/dist/index.d.mts CHANGED
@@ -1,43 +1,5 @@
1
- /** Configuration options for GhostFill */
2
- interface GhostFillOptions {
3
- /** API key (optional — can be set via settings UI) */
4
- apiKey?: string;
5
- /** Keyboard shortcut to toggle GhostFill (default: "Alt+G") */
6
- shortcut?: string;
7
- /** Custom system prompt to prepend */
8
- systemPrompt?: string;
9
- }
10
- /** A detected form field */
11
- interface DetectedField {
12
- /** The DOM element */
13
- element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
14
- /** Field type: text, email, number, select, textarea, checkbox, radio, date, etc. */
15
- type: string;
16
- /** Field name attribute */
17
- name: string;
18
- /** Field label (from <label>, aria-label, or placeholder) */
19
- label: string;
20
- /** For <select>, the available options */
21
- options?: string[];
22
- /** Whether the field is required */
23
- required: boolean;
24
- /** Current value */
25
- currentValue: string;
26
- /** Min/max for number/date fields */
27
- min?: string;
28
- max?: string;
29
- /** Pattern attribute */
30
- pattern?: string;
31
- }
32
- /** Field data returned by the AI */
33
- interface FieldFillData {
34
- /** Index matching the DetectedField array */
35
- index: number;
36
- /** Value to fill */
37
- value: string;
38
- /** For checkboxes: whether to check */
39
- checked?: boolean;
40
- }
1
+ import { G as GhostFillAIOptions, P as Provider, a as GhostFillOptions } from './types-B3DGbZyx.mjs';
2
+ export { D as DetectedField, F as FieldFillData, b as GhostFillAIRequest, c as GhostFillPromptField, d as PROVIDERS } from './types-B3DGbZyx.mjs';
41
3
 
42
4
  /**
43
5
  * Initialize GhostFill — adds a floating ghost icon to the page.
@@ -57,9 +19,12 @@ declare function init(options?: GhostFillOptions): {
57
19
  declare function fill(params: {
58
20
  container: HTMLElement;
59
21
  prompt?: string;
22
+ ai?: GhostFillAIOptions;
23
+ provider?: Provider;
24
+ systemPrompt?: string;
60
25
  }): Promise<{
61
26
  filled: number;
62
27
  errors: string[];
63
28
  }>;
64
29
 
65
- export { type DetectedField, type FieldFillData, type GhostFillOptions, fill, init };
30
+ export { GhostFillAIOptions, GhostFillOptions, Provider, fill, init };
package/dist/index.d.ts CHANGED
@@ -1,43 +1,5 @@
1
- /** Configuration options for GhostFill */
2
- interface GhostFillOptions {
3
- /** API key (optional — can be set via settings UI) */
4
- apiKey?: string;
5
- /** Keyboard shortcut to toggle GhostFill (default: "Alt+G") */
6
- shortcut?: string;
7
- /** Custom system prompt to prepend */
8
- systemPrompt?: string;
9
- }
10
- /** A detected form field */
11
- interface DetectedField {
12
- /** The DOM element */
13
- element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
14
- /** Field type: text, email, number, select, textarea, checkbox, radio, date, etc. */
15
- type: string;
16
- /** Field name attribute */
17
- name: string;
18
- /** Field label (from <label>, aria-label, or placeholder) */
19
- label: string;
20
- /** For <select>, the available options */
21
- options?: string[];
22
- /** Whether the field is required */
23
- required: boolean;
24
- /** Current value */
25
- currentValue: string;
26
- /** Min/max for number/date fields */
27
- min?: string;
28
- max?: string;
29
- /** Pattern attribute */
30
- pattern?: string;
31
- }
32
- /** Field data returned by the AI */
33
- interface FieldFillData {
34
- /** Index matching the DetectedField array */
35
- index: number;
36
- /** Value to fill */
37
- value: string;
38
- /** For checkboxes: whether to check */
39
- checked?: boolean;
40
- }
1
+ import { G as GhostFillAIOptions, P as Provider, a as GhostFillOptions } from './types-B3DGbZyx.js';
2
+ export { D as DetectedField, F as FieldFillData, b as GhostFillAIRequest, c as GhostFillPromptField, d as PROVIDERS } from './types-B3DGbZyx.js';
41
3
 
42
4
  /**
43
5
  * Initialize GhostFill — adds a floating ghost icon to the page.
@@ -57,9 +19,12 @@ declare function init(options?: GhostFillOptions): {
57
19
  declare function fill(params: {
58
20
  container: HTMLElement;
59
21
  prompt?: string;
22
+ ai?: GhostFillAIOptions;
23
+ provider?: Provider;
24
+ systemPrompt?: string;
60
25
  }): Promise<{
61
26
  filled: number;
62
27
  errors: string[];
63
28
  }>;
64
29
 
65
- export { type DetectedField, type FieldFillData, type GhostFillOptions, fill, init };
30
+ export { GhostFillAIOptions, GhostFillOptions, Provider, fill, init };