@1dolinski/fastforms 0.2.0 → 0.3.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/README.md +52 -15
- package/SKILL.md +36 -27
- package/bin/fastforms.js +136 -13
- package/lib/fill.js +84 -59
- package/lib/local.js +44 -3
- package/lib/personas.js +49 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# fastforms
|
|
2
2
|
|
|
3
|
-
Fill any form fast. Manage multiple personas locally, pick the right
|
|
3
|
+
Fill any form fast. Manage multiple personas locally, pick the right ones at fill time.
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
@@ -8,9 +8,8 @@ Fill any form fast. Manage multiple personas locally, pick the right one at fill
|
|
|
8
8
|
# 1. Create your first user + business persona
|
|
9
9
|
npx @1dolinski/fastforms init
|
|
10
10
|
|
|
11
|
-
# 2. Add
|
|
12
|
-
npx @1dolinski/fastforms add
|
|
13
|
-
npx @1dolinski/fastforms add business
|
|
11
|
+
# 2. Add a form persona (org context + form-specific answers)
|
|
12
|
+
npx @1dolinski/fastforms add form
|
|
14
13
|
|
|
15
14
|
# 3. Enable remote debugging in Chrome
|
|
16
15
|
# Open chrome://inspect/#remote-debugging and toggle it on
|
|
@@ -22,15 +21,27 @@ npx @1dolinski/fastforms fill https://example.com/apply
|
|
|
22
21
|
## How it works
|
|
23
22
|
|
|
24
23
|
1. **`fastforms init`** walks you through creating user + business personas interactively
|
|
25
|
-
2.
|
|
26
|
-
3.
|
|
27
|
-
4.
|
|
24
|
+
2. **`fastforms add form`** captures the form's org, purpose, and form-specific answers
|
|
25
|
+
3. Personas are saved as individual JSON files in `.fastforms/`
|
|
26
|
+
4. **`fastforms fill <url>`** connects to Chrome, picks personas, fills by label matching
|
|
27
|
+
5. Form personas auto-match by URL — no need to specify which one
|
|
28
|
+
6. **Review and submit manually** in Chrome
|
|
28
29
|
|
|
29
30
|
## Requirements
|
|
30
31
|
|
|
31
32
|
- Chrome >= 144
|
|
32
33
|
- Node.js >= 18
|
|
33
34
|
|
|
35
|
+
## Persona types
|
|
36
|
+
|
|
37
|
+
| Type | What it captures | Example |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| **User** | Who you are | Name, email, role, GitHub, bio |
|
|
40
|
+
| **Business** | What you're building | Company, product, traction, one-liner |
|
|
41
|
+
| **Form** | Who's asking & why | Org, purpose, form-specific answers |
|
|
42
|
+
|
|
43
|
+
Form persona facts **override** user/business data. "What attracts you to Nitro" belongs on the Nitro form persona, not on your user persona.
|
|
44
|
+
|
|
34
45
|
## Commands
|
|
35
46
|
|
|
36
47
|
| Command | Description |
|
|
@@ -38,6 +49,7 @@ npx @1dolinski/fastforms fill https://example.com/apply
|
|
|
38
49
|
| `fastforms init` | Create your first user + business persona |
|
|
39
50
|
| `fastforms add user` | Add another user persona |
|
|
40
51
|
| `fastforms add business` | Add another business persona |
|
|
52
|
+
| `fastforms add form` | Add a form persona (org + answers) |
|
|
41
53
|
| `fastforms list` | Show all saved personas |
|
|
42
54
|
| `fastforms fill <url>` | Fill any form (pick from personas) |
|
|
43
55
|
| `fastforms edit` | Edit an existing persona |
|
|
@@ -50,6 +62,7 @@ npx @1dolinski/fastforms fill https://example.com/apply
|
|
|
50
62
|
|---|---|
|
|
51
63
|
| `--user <hint>` | Pre-select user persona by name |
|
|
52
64
|
| `--business <hint>` | Pre-select business persona by name |
|
|
65
|
+
| `--form <hint>` | Pre-select form persona by name |
|
|
53
66
|
| `--web` | Use web app personas instead of local files |
|
|
54
67
|
| `--dir <path>` | Custom persona directory path |
|
|
55
68
|
| `--port <port>` | Chrome debug port (auto-detected) |
|
|
@@ -59,15 +72,18 @@ npx @1dolinski/fastforms fill https://example.com/apply
|
|
|
59
72
|
```
|
|
60
73
|
.fastforms/
|
|
61
74
|
users/
|
|
62
|
-
chris.json
|
|
63
|
-
work-chris.json
|
|
75
|
+
chris.json
|
|
76
|
+
work-chris.json
|
|
64
77
|
businesses/
|
|
65
|
-
apinow.json
|
|
66
|
-
sideproject.json
|
|
67
|
-
|
|
78
|
+
apinow.json
|
|
79
|
+
sideproject.json
|
|
80
|
+
forms/
|
|
81
|
+
nitro-accelerator.json
|
|
82
|
+
yc-application.json
|
|
83
|
+
defaults.json
|
|
68
84
|
```
|
|
69
85
|
|
|
70
|
-
|
|
86
|
+
### User persona (`users/chris.json`)
|
|
71
87
|
|
|
72
88
|
```json
|
|
73
89
|
{
|
|
@@ -86,7 +102,7 @@ Each user JSON file — just fill in what you have:
|
|
|
86
102
|
}
|
|
87
103
|
```
|
|
88
104
|
|
|
89
|
-
|
|
105
|
+
### Business persona (`businesses/apinow.json`)
|
|
90
106
|
|
|
91
107
|
```json
|
|
92
108
|
{
|
|
@@ -98,6 +114,27 @@ Each business JSON file:
|
|
|
98
114
|
}
|
|
99
115
|
```
|
|
100
116
|
|
|
117
|
+
### Form persona (`forms/nitro-accelerator.json`)
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"name": "Nitro Accelerator",
|
|
122
|
+
"urls": ["nitroacc.xyz"],
|
|
123
|
+
"organization": "Nitro",
|
|
124
|
+
"purpose": "Crypto accelerator application for early-stage founders",
|
|
125
|
+
"notes": "NYC-based, 1-month residency",
|
|
126
|
+
"deadline": "2026-04-01",
|
|
127
|
+
"facts": {
|
|
128
|
+
"attracts": "The density of high-signal founders and the NYC residency",
|
|
129
|
+
"mentor": "Vitalik — his work on public goods aligns with our mission",
|
|
130
|
+
"spend": "Engineering hires and infrastructure for mainnet launch",
|
|
131
|
+
"last week": "Shipped v2 of the API, onboarded 3 beta customers"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The `facts` on a form persona are form-specific answers keyed by label hints. They override user/business data when a form field matches.
|
|
137
|
+
|
|
101
138
|
## Web app (optional)
|
|
102
139
|
|
|
103
140
|
You can also manage personas in the web UI at [293-fastforms.vercel.app/persona](https://293-fastforms.vercel.app/persona) and use `--web` flag to pull from there. The web app has an "Export for CLI" button to download your personas as local JSON files.
|
|
@@ -116,7 +153,7 @@ node bin/fastforms.js fill https://example.com/apply
|
|
|
116
153
|
|
|
117
154
|
### Adding a new form mapping
|
|
118
155
|
|
|
119
|
-
1. Add a `buildXxxData(user, biz)` function in `lib/fill.js`
|
|
156
|
+
1. Add a `buildXxxData(user, biz, form)` function in `lib/fill.js`
|
|
120
157
|
2. Add a URL check in `fillForm()`
|
|
121
158
|
3. Test with `node bin/fastforms.js fill <url>`
|
|
122
159
|
|
package/SKILL.md
CHANGED
|
@@ -11,15 +11,18 @@ Use this skill when the user says any of:
|
|
|
11
11
|
- "fill this form with my persona"
|
|
12
12
|
- "use fastforms", "fastforms fill"
|
|
13
13
|
- "set up my personas", "init fastforms"
|
|
14
|
-
- "add a persona", "add
|
|
14
|
+
- "add a persona", "add a form persona"
|
|
15
15
|
|
|
16
16
|
## What it does
|
|
17
17
|
|
|
18
18
|
`fastforms` is a CLI tool that:
|
|
19
19
|
|
|
20
|
-
1. Manages
|
|
20
|
+
1. Manages three types of personas locally in `.fastforms/`:
|
|
21
|
+
- **User** — who you are (name, email, bio, skills)
|
|
22
|
+
- **Business** — what you're building (company, product, traction)
|
|
23
|
+
- **Form** — who's asking and why (org, purpose, form-specific answers)
|
|
21
24
|
2. Connects to Chrome via the DevTools Protocol
|
|
22
|
-
3. Lets you pick which user + business persona to use at fill time
|
|
25
|
+
3. Lets you pick which user + business + form persona to use at fill time
|
|
23
26
|
4. Fills any form using label-matching — never submits
|
|
24
27
|
|
|
25
28
|
## Quick start
|
|
@@ -28,9 +31,8 @@ Use this skill when the user says any of:
|
|
|
28
31
|
# 1. Create your first user + business persona
|
|
29
32
|
npx @1dolinski/fastforms init
|
|
30
33
|
|
|
31
|
-
# 2. Add
|
|
32
|
-
npx @1dolinski/fastforms add
|
|
33
|
-
npx @1dolinski/fastforms add business
|
|
34
|
+
# 2. Add a form persona with org context and form-specific answers
|
|
35
|
+
npx @1dolinski/fastforms add form
|
|
34
36
|
|
|
35
37
|
# 3. Enable remote debugging in Chrome
|
|
36
38
|
# Open chrome://inspect/#remote-debugging
|
|
@@ -43,65 +45,72 @@ npx @1dolinski/fastforms fill https://example.com/apply
|
|
|
43
45
|
|
|
44
46
|
### `npx @1dolinski/fastforms init`
|
|
45
47
|
|
|
46
|
-
Walks through creating a user + business persona
|
|
48
|
+
Walks through creating a user + business persona (optionally a form persona too).
|
|
47
49
|
|
|
48
|
-
### `npx @1dolinski/fastforms add user|business`
|
|
50
|
+
### `npx @1dolinski/fastforms add user|business|form`
|
|
49
51
|
|
|
50
|
-
Add another
|
|
52
|
+
Add another persona of any type.
|
|
51
53
|
|
|
52
54
|
### `npx @1dolinski/fastforms list`
|
|
53
55
|
|
|
54
|
-
Show all saved personas.
|
|
56
|
+
Show all saved personas across all types.
|
|
55
57
|
|
|
56
58
|
### `npx @1dolinski/fastforms fill <url>`
|
|
57
59
|
|
|
58
|
-
Fills any form. If you have multiple personas, prompts you to pick
|
|
60
|
+
Fills any form. If you have multiple personas, prompts you to pick. Form personas auto-match by URL.
|
|
59
61
|
|
|
60
62
|
Options:
|
|
61
63
|
- `--user <hint>` — pre-select a user persona by name
|
|
62
64
|
- `--business <hint>` — pre-select a business persona by name
|
|
65
|
+
- `--form <hint>` — pre-select a form persona by name
|
|
63
66
|
- `--web` — use web app personas (https://293-fastforms.vercel.app) instead of local files
|
|
64
67
|
- `--dir <path>` — custom path to persona directory
|
|
65
68
|
- `--port <port>` — Chrome debug port (auto-detected by default)
|
|
66
69
|
|
|
67
70
|
### `npx @1dolinski/fastforms edit`
|
|
68
71
|
|
|
69
|
-
Pick
|
|
72
|
+
Pick any persona to edit interactively.
|
|
70
73
|
|
|
71
74
|
### `npx @1dolinski/fastforms remove`
|
|
72
75
|
|
|
73
|
-
Pick
|
|
76
|
+
Pick any persona to delete.
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
## Persona types
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
### User persona
|
|
81
|
+
Who you are. Name, email, role, GitHub, LinkedIn, bio, custom facts.
|
|
78
82
|
|
|
79
|
-
|
|
83
|
+
### Business persona
|
|
84
|
+
What you're building. Company, product, one-liner, traction, business model.
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
### Form persona
|
|
87
|
+
Who's asking and why. The organization that owns the form, the form's purpose, and **form-specific answers** that override user/business data. For example, "what attracts you to Nitro" is a Nitro-specific answer — it belongs on the form persona, not on your user or business persona.
|
|
88
|
+
|
|
89
|
+
Form personas auto-match by URL. If your form persona has `urls: ["nitroacc.xyz"]` and you fill `https://nitroacc.xyz/apply`, it auto-selects.
|
|
83
90
|
|
|
84
91
|
## How it works
|
|
85
92
|
|
|
86
|
-
1. Reads all personas from `.fastforms/users
|
|
93
|
+
1. Reads all personas from `.fastforms/users/`, `.fastforms/businesses/`, and `.fastforms/forms/`
|
|
87
94
|
2. If multiple, prompts you to pick which user + business persona to use
|
|
88
|
-
3. Auto-
|
|
89
|
-
4.
|
|
90
|
-
5.
|
|
91
|
-
6.
|
|
95
|
+
3. Auto-matches form persona by URL (or prompts if multiple)
|
|
96
|
+
4. Auto-discovers Chrome's debug port from `DevToolsActivePort`
|
|
97
|
+
5. Opens (or reuses) the target form URL tab
|
|
98
|
+
6. Fills using label-matching. Form persona facts are checked first as overrides.
|
|
99
|
+
7. Shows what was filled, what was skipped. **Never submits.**
|
|
92
100
|
|
|
93
101
|
## Agent instructions
|
|
94
102
|
|
|
95
103
|
When the user asks you to fill a form:
|
|
96
104
|
|
|
97
105
|
1. Check if `.fastforms/` exists. If not, run `npx @1dolinski/fastforms init`
|
|
98
|
-
2.
|
|
99
|
-
3.
|
|
100
|
-
4.
|
|
106
|
+
2. If the form is new, suggest `npx @1dolinski/fastforms add form` to capture form-specific context
|
|
107
|
+
3. Run `npx @1dolinski/fastforms fill <the-url>`
|
|
108
|
+
4. If Chrome debugging isn't enabled, tell the user to open `chrome://inspect/#remote-debugging`
|
|
109
|
+
5. After filling, tell the user to review in Chrome and submit manually
|
|
101
110
|
|
|
102
111
|
When the user wants to add a persona:
|
|
103
112
|
|
|
104
|
-
1. Run `npx @1dolinski/fastforms add user
|
|
113
|
+
1. Run `npx @1dolinski/fastforms add user|business|form`
|
|
105
114
|
|
|
106
115
|
When the user wants to see their personas:
|
|
107
116
|
|
package/bin/fastforms.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
connectToChrome,
|
|
11
11
|
pullPersonas,
|
|
12
12
|
selectPersonas,
|
|
13
|
+
selectFormPersona,
|
|
13
14
|
showPersonaDetails,
|
|
14
15
|
offerSetDefaults,
|
|
15
16
|
offerOpenPersonaManager,
|
|
@@ -21,12 +22,14 @@ import {
|
|
|
21
22
|
loadLocalPersonas,
|
|
22
23
|
saveUserPersona,
|
|
23
24
|
saveBusinessPersona,
|
|
25
|
+
saveFormPersona,
|
|
24
26
|
deletePersonaFile,
|
|
25
27
|
listPersonaFiles,
|
|
26
28
|
loadDefaults,
|
|
27
29
|
saveDefaults,
|
|
28
30
|
userTemplate,
|
|
29
31
|
businessTemplate,
|
|
32
|
+
formTemplate,
|
|
30
33
|
} from "../lib/local.js";
|
|
31
34
|
|
|
32
35
|
const args = process.argv.slice(2);
|
|
@@ -69,6 +72,7 @@ function help() {
|
|
|
69
72
|
fastforms init Create your first user + business persona
|
|
70
73
|
fastforms add user Add another user persona
|
|
71
74
|
fastforms add business Add another business persona
|
|
75
|
+
fastforms add form Add a form persona (org + purpose + answers)
|
|
72
76
|
fastforms list List all personas
|
|
73
77
|
fastforms edit Edit an existing persona
|
|
74
78
|
fastforms remove Remove a persona
|
|
@@ -81,11 +85,17 @@ function help() {
|
|
|
81
85
|
--dir <path> Path to .fastforms/ directory (default: auto-detect)
|
|
82
86
|
--user <hint> User persona name/hint to pre-select
|
|
83
87
|
--business <hint> Business persona name/hint to pre-select
|
|
88
|
+
--form <hint> Form persona name/hint to pre-select
|
|
84
89
|
--port <port> Chrome debug port (auto-detected by default)
|
|
85
90
|
|
|
91
|
+
Persona types:
|
|
92
|
+
user — who you are (name, email, bio, skills)
|
|
93
|
+
business — what you're building (company, product, traction)
|
|
94
|
+
form — who's asking and why (org, purpose, form-specific answers)
|
|
95
|
+
|
|
86
96
|
Quick start:
|
|
87
97
|
1. npx @1dolinski/fastforms init
|
|
88
|
-
2. npx @1dolinski/fastforms add
|
|
98
|
+
2. npx @1dolinski/fastforms add form # add form-specific context
|
|
89
99
|
3. Enable remote debugging: chrome://inspect/#remote-debugging
|
|
90
100
|
4. npx @1dolinski/fastforms fill https://example.com/apply
|
|
91
101
|
`);
|
|
@@ -172,6 +182,55 @@ async function promptBusiness(existing) {
|
|
|
172
182
|
return b;
|
|
173
183
|
}
|
|
174
184
|
|
|
185
|
+
async function promptForm(existing) {
|
|
186
|
+
const ep = existing?.profile || {};
|
|
187
|
+
const f = formTemplate();
|
|
188
|
+
|
|
189
|
+
const show = (val) => val ? ` [${String(val).slice(0, 40)}]` : "";
|
|
190
|
+
|
|
191
|
+
f.name = await ask(` Form name (e.g. "Nitro Accelerator")${show(existing?.name)}: `, existing?.name || "");
|
|
192
|
+
f.organization = await ask(` Organization${show(ep.organization)}: `, ep.organization || "");
|
|
193
|
+
f.purpose = await ask(` Purpose (e.g. "crypto accelerator application")${show(ep.purpose)}: `, ep.purpose || "");
|
|
194
|
+
|
|
195
|
+
const existingUrls = existing?.urls || [];
|
|
196
|
+
if (existingUrls.length) {
|
|
197
|
+
console.log(`\n Current URLs: ${existingUrls.join(", ")}`);
|
|
198
|
+
}
|
|
199
|
+
console.log("\n Add URL patterns this form matches (press Enter to finish):\n");
|
|
200
|
+
f.urls = [...existingUrls];
|
|
201
|
+
while (true) {
|
|
202
|
+
const url = await ask(" url: ");
|
|
203
|
+
if (!url) break;
|
|
204
|
+
if (!f.urls.includes(url)) f.urls.push(url);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
f.notes = await ask(` Notes / context${show(ep.notes)}: `, ep.notes || "");
|
|
208
|
+
f.deadline = await ask(` Deadline${show(ep.deadline)}: `, ep.deadline || "");
|
|
209
|
+
f.requirements = await ask(` Requirements${show(ep.requirements)}: `, ep.requirements || "");
|
|
210
|
+
|
|
211
|
+
const existingFacts = {};
|
|
212
|
+
for (const cf of (existing?.customFacts || [])) {
|
|
213
|
+
if (cf.enabled !== false && cf.key) existingFacts[cf.key] = cf.value;
|
|
214
|
+
}
|
|
215
|
+
f.facts = { ...existingFacts };
|
|
216
|
+
|
|
217
|
+
if (Object.keys(f.facts).length) {
|
|
218
|
+
console.log("\n Current form-specific answers:");
|
|
219
|
+
for (const [k, v] of Object.entries(f.facts)) console.log(` ${k} = ${v}`);
|
|
220
|
+
}
|
|
221
|
+
console.log("\n Add form-specific answers (label hint = answer). Press Enter to finish.");
|
|
222
|
+
console.log(" These override user/business data for matching fields.\n");
|
|
223
|
+
while (true) {
|
|
224
|
+
const raw = await ask(" answer: ");
|
|
225
|
+
if (!raw) break;
|
|
226
|
+
const eq = raw.indexOf("=");
|
|
227
|
+
if (eq === -1) { console.log(" Use format: field hint = answer"); continue; }
|
|
228
|
+
f.facts[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return f;
|
|
232
|
+
}
|
|
233
|
+
|
|
175
234
|
// ---------------------------------------------------------------------------
|
|
176
235
|
// init — first-time setup: one user + one business
|
|
177
236
|
// ---------------------------------------------------------------------------
|
|
@@ -185,7 +244,7 @@ async function init() {
|
|
|
185
244
|
if (existingUsers.length) {
|
|
186
245
|
const ans = await ask(` ${existingUsers.length} persona(s) already exist. Add another? [Y/n]: `);
|
|
187
246
|
if (ans.toLowerCase() === "n") {
|
|
188
|
-
console.log(" Use 'fastforms add user
|
|
247
|
+
console.log(" Use 'fastforms add user|business|form' to add more.\n");
|
|
189
248
|
closeRL();
|
|
190
249
|
return;
|
|
191
250
|
}
|
|
@@ -202,7 +261,16 @@ async function init() {
|
|
|
202
261
|
const bizSlug = saveBusinessPersona(dir, biz);
|
|
203
262
|
console.log(`\n Saved businesses/${bizSlug}.json`);
|
|
204
263
|
|
|
264
|
+
const addForm = await ask("\n Add a form persona now? (org + purpose + form-specific answers) [y/N]: ");
|
|
265
|
+
if (addForm.toLowerCase() === "y") {
|
|
266
|
+
console.log("\n --- Form persona ---\n");
|
|
267
|
+
const form = await promptForm();
|
|
268
|
+
const formSlug = saveFormPersona(dir, form);
|
|
269
|
+
console.log(`\n Saved forms/${formSlug}.json`);
|
|
270
|
+
}
|
|
271
|
+
|
|
205
272
|
console.log(`\n Personas saved to ${dir}/`);
|
|
273
|
+
console.log(" Tip: add form-specific context with 'fastforms add form'");
|
|
206
274
|
console.log(" Next: npx @1dolinski/fastforms fill <url>\n");
|
|
207
275
|
closeRL();
|
|
208
276
|
}
|
|
@@ -213,8 +281,8 @@ async function init() {
|
|
|
213
281
|
|
|
214
282
|
async function addPersona() {
|
|
215
283
|
const type = args[1];
|
|
216
|
-
if (type !== "user" && type !== "business") {
|
|
217
|
-
console.error(" Usage: fastforms add user|business\n");
|
|
284
|
+
if (type !== "user" && type !== "business" && type !== "form") {
|
|
285
|
+
console.error(" Usage: fastforms add user|business|form\n");
|
|
218
286
|
process.exit(1);
|
|
219
287
|
}
|
|
220
288
|
|
|
@@ -226,11 +294,16 @@ async function addPersona() {
|
|
|
226
294
|
const user = await promptUser();
|
|
227
295
|
const slug = saveUserPersona(dir, user);
|
|
228
296
|
console.log(`\n Saved users/${slug}.json to ${dir}/\n`);
|
|
229
|
-
} else {
|
|
297
|
+
} else if (type === "business") {
|
|
230
298
|
console.log("\n --- New business persona ---\n");
|
|
231
299
|
const biz = await promptBusiness();
|
|
232
300
|
const slug = saveBusinessPersona(dir, biz);
|
|
233
301
|
console.log(`\n Saved businesses/${slug}.json to ${dir}/\n`);
|
|
302
|
+
} else {
|
|
303
|
+
console.log("\n --- New form persona ---\n");
|
|
304
|
+
const form = await promptForm();
|
|
305
|
+
const slug = saveFormPersona(dir, form);
|
|
306
|
+
console.log(`\n Saved forms/${slug}.json to ${dir}/\n`);
|
|
234
307
|
}
|
|
235
308
|
closeRL();
|
|
236
309
|
}
|
|
@@ -250,6 +323,7 @@ function listAll() {
|
|
|
250
323
|
|
|
251
324
|
const users = listPersonaFiles(dir, "user");
|
|
252
325
|
const businesses = listPersonaFiles(dir, "business");
|
|
326
|
+
const forms = listPersonaFiles(dir, "form");
|
|
253
327
|
|
|
254
328
|
console.log(`\n Personas in ${dir}/\n`);
|
|
255
329
|
|
|
@@ -276,6 +350,20 @@ function listAll() {
|
|
|
276
350
|
}
|
|
277
351
|
|
|
278
352
|
console.log();
|
|
353
|
+
|
|
354
|
+
if (forms.length) {
|
|
355
|
+
console.log(" Form personas:");
|
|
356
|
+
for (const f of forms) {
|
|
357
|
+
const urls = f.data?.urls?.length ? ` [${f.data.urls.join(", ")}]` : "";
|
|
358
|
+
const purpose = f.data?.purpose ? ` — ${f.data.purpose}` : "";
|
|
359
|
+
const facts = f.data?.facts ? Object.keys(f.data.facts).length : 0;
|
|
360
|
+
console.log(` ${f.slug}${urls}${purpose}${facts ? ` (${facts} answers)` : ""}`);
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
console.log(" No form personas. Run: fastforms add form");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.log();
|
|
279
367
|
}
|
|
280
368
|
|
|
281
369
|
// ---------------------------------------------------------------------------
|
|
@@ -291,9 +379,11 @@ async function edit() {
|
|
|
291
379
|
|
|
292
380
|
const users = listPersonaFiles(dir, "user");
|
|
293
381
|
const businesses = listPersonaFiles(dir, "business");
|
|
382
|
+
const forms = listPersonaFiles(dir, "form");
|
|
294
383
|
const all = [
|
|
295
384
|
...users.map((u) => ({ ...u, type: "user", label: `user: ${u.slug} (${u.data?.fullName || u.data?.name || "?"})` })),
|
|
296
385
|
...businesses.map((b) => ({ ...b, type: "business", label: `biz: ${b.slug} (${b.data?.name || "?"})` })),
|
|
386
|
+
...forms.map((f) => ({ ...f, type: "form", label: `form: ${f.slug} (${f.data?.organization || f.data?.name || "?"})` })),
|
|
297
387
|
];
|
|
298
388
|
|
|
299
389
|
if (!all.length) {
|
|
@@ -317,13 +407,20 @@ async function edit() {
|
|
|
317
407
|
if (updated.name !== picked.data?.name) deletePersonaFile(dir, "user", picked.slug);
|
|
318
408
|
const slug = saveUserPersona(dir, updated);
|
|
319
409
|
console.log(`\n Updated users/${slug}.json\n`);
|
|
320
|
-
} else {
|
|
410
|
+
} else if (picked.type === "business") {
|
|
321
411
|
const existing = dump.businessPersonas.find((p) => p.name === picked.data?.name) || null;
|
|
322
412
|
console.log(`\n --- Edit business: ${picked.slug} ---\n`);
|
|
323
413
|
const updated = await promptBusiness(existing);
|
|
324
414
|
if (updated.name !== picked.data?.name) deletePersonaFile(dir, "business", picked.slug);
|
|
325
415
|
const slug = saveBusinessPersona(dir, updated);
|
|
326
416
|
console.log(`\n Updated businesses/${slug}.json\n`);
|
|
417
|
+
} else {
|
|
418
|
+
const existing = (dump.formPersonas || []).find((p) => p.name === picked.data?.name) || null;
|
|
419
|
+
console.log(`\n --- Edit form: ${picked.slug} ---\n`);
|
|
420
|
+
const updated = await promptForm(existing);
|
|
421
|
+
if (updated.name !== picked.data?.name) deletePersonaFile(dir, "form", picked.slug);
|
|
422
|
+
const slug = saveFormPersona(dir, updated);
|
|
423
|
+
console.log(`\n Updated forms/${slug}.json\n`);
|
|
327
424
|
}
|
|
328
425
|
closeRL();
|
|
329
426
|
}
|
|
@@ -341,9 +438,11 @@ async function remove() {
|
|
|
341
438
|
|
|
342
439
|
const users = listPersonaFiles(dir, "user");
|
|
343
440
|
const businesses = listPersonaFiles(dir, "business");
|
|
441
|
+
const forms = listPersonaFiles(dir, "form");
|
|
344
442
|
const all = [
|
|
345
443
|
...users.map((u) => ({ ...u, type: "user", label: `user: ${u.slug} (${u.data?.fullName || u.data?.name || "?"})` })),
|
|
346
444
|
...businesses.map((b) => ({ ...b, type: "business", label: `biz: ${b.slug} (${b.data?.name || "?"})` })),
|
|
445
|
+
...forms.map((f) => ({ ...f, type: "form", label: `form: ${f.slug} (${f.data?.name || "?"})` })),
|
|
347
446
|
];
|
|
348
447
|
|
|
349
448
|
if (!all.length) {
|
|
@@ -361,7 +460,8 @@ async function remove() {
|
|
|
361
460
|
const confirm = await ask(` Delete ${picked.type}/${picked.slug}? [y/N]: `);
|
|
362
461
|
if (confirm.toLowerCase() === "y") {
|
|
363
462
|
deletePersonaFile(dir, picked.type, picked.slug);
|
|
364
|
-
|
|
463
|
+
const dirName = picked.type === "user" ? "users" : picked.type === "business" ? "businesses" : "forms";
|
|
464
|
+
console.log(` Removed ${dirName}/${picked.slug}.json\n`);
|
|
365
465
|
} else {
|
|
366
466
|
console.log(" Cancelled.\n");
|
|
367
467
|
}
|
|
@@ -408,7 +508,13 @@ async function fill() {
|
|
|
408
508
|
|
|
409
509
|
const personas = dump.personas || [];
|
|
410
510
|
const bizPersonas = dump.businessPersonas || [];
|
|
411
|
-
|
|
511
|
+
const formPersonas = dump.formPersonas || [];
|
|
512
|
+
const counts = [
|
|
513
|
+
`${personas.length} user`,
|
|
514
|
+
`${bizPersonas.length} business`,
|
|
515
|
+
`${formPersonas.length} form`,
|
|
516
|
+
].join(", ");
|
|
517
|
+
console.log(` Found ${counts} persona(s).`);
|
|
412
518
|
|
|
413
519
|
if (!personas.length && !bizPersonas.length) {
|
|
414
520
|
console.error("\n No personas found.");
|
|
@@ -423,15 +529,29 @@ async function fill() {
|
|
|
423
529
|
|
|
424
530
|
const userHint = flag("--user");
|
|
425
531
|
const bizHint = flag("--business");
|
|
532
|
+
const formHint = flag("--form");
|
|
426
533
|
const { user, biz } = await selectPersonas(dump, userHint, bizHint);
|
|
427
534
|
|
|
535
|
+
// Form persona: auto-match by URL, then hint, then interactive
|
|
536
|
+
let form = null;
|
|
537
|
+
if (formPersonas.length) {
|
|
538
|
+
if (formHint) {
|
|
539
|
+
form = formPersonas.find((f) =>
|
|
540
|
+
f.name.toLowerCase().includes(formHint.toLowerCase())
|
|
541
|
+
) || null;
|
|
542
|
+
}
|
|
543
|
+
if (!form) {
|
|
544
|
+
form = await selectFormPersona(formPersonas, formUrl);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
428
548
|
if (!user && !biz) {
|
|
429
549
|
console.error("\n No matching personas.");
|
|
430
550
|
browser.disconnect();
|
|
431
551
|
process.exit(1);
|
|
432
552
|
}
|
|
433
553
|
|
|
434
|
-
showPersonaDetails(user, biz);
|
|
554
|
+
showPersonaDetails(user, biz, form);
|
|
435
555
|
|
|
436
556
|
const pages = await browser.pages();
|
|
437
557
|
const host = new URL(formUrl).host;
|
|
@@ -446,15 +566,18 @@ async function fill() {
|
|
|
446
566
|
console.log(`\n Opened ${formUrl}`);
|
|
447
567
|
}
|
|
448
568
|
|
|
449
|
-
await fillForm(page, formUrl, user, biz);
|
|
569
|
+
await fillForm(page, formUrl, user, biz, form);
|
|
450
570
|
|
|
451
571
|
if (hasFlag("--web")) {
|
|
452
572
|
await offerSetDefaults(user, biz);
|
|
453
573
|
} else {
|
|
454
|
-
// Save defaults to local dir
|
|
455
574
|
const dir = findFastformsDir();
|
|
456
|
-
if (dir
|
|
457
|
-
|
|
575
|
+
if (dir) {
|
|
576
|
+
const patch = {};
|
|
577
|
+
if (user) patch.defaultUser = user.name;
|
|
578
|
+
if (biz) patch.defaultBusiness = biz.name;
|
|
579
|
+
if (form) patch.defaultForm = form.name;
|
|
580
|
+
saveDefaults(dir, patch);
|
|
458
581
|
}
|
|
459
582
|
}
|
|
460
583
|
|
package/lib/fill.js
CHANGED
|
@@ -2,6 +2,14 @@ import { getCustomFact } from "./personas.js";
|
|
|
2
2
|
|
|
3
3
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Look up a value from the form persona's facts first, then fall back.
|
|
7
|
+
* Form persona facts override everything — they're form-specific answers.
|
|
8
|
+
*/
|
|
9
|
+
function formFact(form, key) {
|
|
10
|
+
return getCustomFact(form, key);
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
export async function fillByLabel(page, label, value, opts = {}) {
|
|
6
14
|
if (!value) return false;
|
|
7
15
|
return page.evaluate((lbl, val, ta) => {
|
|
@@ -55,82 +63,95 @@ export async function clickCheckbox(page, textHint) {
|
|
|
55
63
|
}, textHint);
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
export function buildNitroData(user, biz) {
|
|
66
|
+
export function buildNitroData(user, biz, form) {
|
|
59
67
|
const u = user?.profile || {};
|
|
60
68
|
const b = biz?.profile || {};
|
|
61
69
|
const fact = (p, k) => getCustomFact(p, k);
|
|
70
|
+
const ff = (k) => formFact(form, k);
|
|
62
71
|
|
|
63
72
|
return {
|
|
64
73
|
fields: [
|
|
65
|
-
{ label: "Company / Project name", value: b.productName || b.companyName || b.name || "" },
|
|
66
|
-
{ label: "Email", value: u.email || "" },
|
|
67
|
-
{ label: "One-line description", value: b.oneLiner || "" },
|
|
68
|
-
{ label: "What are you building", value: b.solution || [b.problem, b.solution].filter(Boolean).join("\n\n"), textarea: true },
|
|
69
|
-
{ label: "Full Name", value: u.fullName || "" },
|
|
70
|
-
{ label: "Role in the company", value: u.currentRole || "" },
|
|
71
|
-
{ label: "X Handle", value: fact(user, "x handle") || fact(user, "twitter") || "" },
|
|
72
|
-
{ label: "LinkedIn username", value: u.linkedIn || "" },
|
|
73
|
-
{ label: "Telegram", value: fact(user, "telegram") || "" },
|
|
74
|
-
{ label: "GitHub username", value: u.github || "" },
|
|
75
|
-
{ label: "Why are you the right founder", value: [u.bio, u.keySkills ? `Key skills: ${u.keySkills}` : "", u.favoriteProjects ? `Notable projects: ${u.favoriteProjects}` : ""].filter(Boolean).join("\n\n"), textarea: true },
|
|
76
|
-
{ label: "video content or long form writing", value: u.portfolio || "", textarea: true },
|
|
77
|
-
{ label: "How did the founders meet", value: fact(user, "founders met") || fact(biz, "founders") || `I'm a solo founder building ${b.productName || b.companyName || "this project"} full-time.`, textarea: true },
|
|
78
|
-
{ label: "Current location of founders", value: u.location || b.location || "" },
|
|
79
|
-
{ label: "how do you plan to spend", value: fact(biz, "spend") || fact(biz, "500k") || "", textarea: true },
|
|
80
|
-
{ label: "What problem are you solving", value: b.problem || "", textarea: true },
|
|
81
|
-
{ label: "closest comparables", value: b.differentiators || "", textarea: true },
|
|
82
|
-
{ label: "Product Link", value: b.website || "" },
|
|
83
|
-
{ label: "What changed in the tech or market", value: fact(biz, "timing") || fact(biz, "why now") || b.rawFacts || "", textarea: true },
|
|
84
|
-
{ label: "Dune Dashboard", value: "" },
|
|
85
|
-
{ label: "Google Analytics", value: "", textarea: true },
|
|
86
|
-
{ label: "target segment", value: b.targetUsers || "", textarea: true },
|
|
87
|
-
{ label: "wedge into the market", value: b.businessModel || b.differentiators || "", textarea: true },
|
|
88
|
-
{ label: "traction have you achieved", value: b.traction || "", textarea: true },
|
|
89
|
-
{ label: "Runway at current burn", value: "12" },
|
|
90
|
-
{ label: "What attracts you the most about Nitro", value: fact(biz, "nitro") || fact(biz, "accelerator") || `The density of high-signal founders and the NYC residency. ${b.productName || "Our product"} is at a stage where concentrated feedback from experienced operators would dramatically accelerate our trajectory.`, textarea: true },
|
|
91
|
-
{ label: "Pick one of the mentors", value: fact(biz, "mentor") || "", textarea: true },
|
|
92
|
-
{ label: "What did you get done last week", value: fact(biz, "last week") || fact(user, "last week") || "", textarea: true },
|
|
74
|
+
{ label: "Company / Project name", value: ff("company") || b.productName || b.companyName || b.name || "" },
|
|
75
|
+
{ label: "Email", value: ff("email") || u.email || "" },
|
|
76
|
+
{ label: "One-line description", value: ff("one-line") || b.oneLiner || "" },
|
|
77
|
+
{ label: "What are you building", value: ff("building") || b.solution || [b.problem, b.solution].filter(Boolean).join("\n\n"), textarea: true },
|
|
78
|
+
{ label: "Full Name", value: ff("full name") || u.fullName || "" },
|
|
79
|
+
{ label: "Role in the company", value: ff("role") || u.currentRole || "" },
|
|
80
|
+
{ label: "X Handle", value: ff("x handle") || fact(user, "x handle") || fact(user, "twitter") || "" },
|
|
81
|
+
{ label: "LinkedIn username", value: ff("linkedin") || u.linkedIn || "" },
|
|
82
|
+
{ label: "Telegram", value: ff("telegram") || fact(user, "telegram") || "" },
|
|
83
|
+
{ label: "GitHub username", value: ff("github") || u.github || "" },
|
|
84
|
+
{ label: "Why are you the right founder", value: ff("right founder") || [u.bio, u.keySkills ? `Key skills: ${u.keySkills}` : "", u.favoriteProjects ? `Notable projects: ${u.favoriteProjects}` : ""].filter(Boolean).join("\n\n"), textarea: true },
|
|
85
|
+
{ label: "video content or long form writing", value: ff("video") || ff("writing") || u.portfolio || "", textarea: true },
|
|
86
|
+
{ label: "How did the founders meet", value: ff("founders meet") || ff("founders met") || fact(user, "founders met") || fact(biz, "founders") || `I'm a solo founder building ${b.productName || b.companyName || "this project"} full-time.`, textarea: true },
|
|
87
|
+
{ label: "Current location of founders", value: ff("location") || u.location || b.location || "" },
|
|
88
|
+
{ label: "how do you plan to spend", value: ff("spend") || ff("500k") || fact(biz, "spend") || fact(biz, "500k") || "", textarea: true },
|
|
89
|
+
{ label: "What problem are you solving", value: ff("problem") || b.problem || "", textarea: true },
|
|
90
|
+
{ label: "closest comparables", value: ff("comparables") || ff("competitors") || b.differentiators || "", textarea: true },
|
|
91
|
+
{ label: "Product Link", value: ff("product link") || b.website || "" },
|
|
92
|
+
{ label: "What changed in the tech or market", value: ff("timing") || ff("why now") || fact(biz, "timing") || fact(biz, "why now") || b.rawFacts || "", textarea: true },
|
|
93
|
+
{ label: "Dune Dashboard", value: ff("dune") || "" },
|
|
94
|
+
{ label: "Google Analytics", value: ff("analytics") || "", textarea: true },
|
|
95
|
+
{ label: "target segment", value: ff("target") || b.targetUsers || "", textarea: true },
|
|
96
|
+
{ label: "wedge into the market", value: ff("wedge") || ff("market") || b.businessModel || b.differentiators || "", textarea: true },
|
|
97
|
+
{ label: "traction have you achieved", value: ff("traction") || b.traction || "", textarea: true },
|
|
98
|
+
{ label: "Runway at current burn", value: ff("runway") || "12" },
|
|
99
|
+
{ label: "What attracts you the most about Nitro", value: ff("attracts") || ff("nitro") || fact(biz, "nitro") || fact(biz, "accelerator") || `The density of high-signal founders and the NYC residency. ${b.productName || "Our product"} is at a stage where concentrated feedback from experienced operators would dramatically accelerate our trajectory.`, textarea: true },
|
|
100
|
+
{ label: "Pick one of the mentors", value: ff("mentor") || fact(biz, "mentor") || "", textarea: true },
|
|
101
|
+
{ label: "What did you get done last week", value: ff("last week") || fact(biz, "last week") || fact(user, "last week") || "", textarea: true },
|
|
93
102
|
],
|
|
94
103
|
radios: [
|
|
95
|
-
{ section: "committed full-time", value: "yes" },
|
|
96
|
-
{ section: "raised funding before", value: "no" },
|
|
97
|
-
{ section: "currently fundraising", value: "yes" },
|
|
98
|
-
{ section: "exclusively in Nitro", value: "yes" },
|
|
99
|
-
{ section: "attend the full 1-month NYC", value: "yes" },
|
|
104
|
+
{ section: "committed full-time", value: ff("full-time") || "yes" },
|
|
105
|
+
{ section: "raised funding before", value: ff("raised funding") || "no" },
|
|
106
|
+
{ section: "currently fundraising", value: ff("fundraising") || "yes" },
|
|
107
|
+
{ section: "exclusively in Nitro", value: ff("exclusively") || "yes" },
|
|
108
|
+
{ section: "attend the full 1-month NYC", value: ff("attend") || "yes" },
|
|
100
109
|
],
|
|
101
110
|
checkboxes: ["MVP / demo exists"],
|
|
102
111
|
};
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
export async function genericFill(page, user, biz) {
|
|
114
|
+
export async function genericFill(page, user, biz, form) {
|
|
106
115
|
const u = user?.profile || {};
|
|
107
116
|
const b = biz?.profile || {};
|
|
108
117
|
const fact = (p, k) => getCustomFact(p, k);
|
|
118
|
+
const ff = (k) => formFact(form, k);
|
|
109
119
|
|
|
110
120
|
const fieldMap = [
|
|
111
|
-
{ hints: ["company", "project name", "organization"], value: b.productName || b.companyName || b.name || "" },
|
|
112
|
-
{ hints: ["email"], value: u.email || "" },
|
|
113
|
-
{ hints: ["full name", "your name", "first name"], value: u.fullName || "" },
|
|
114
|
-
{ hints: ["one-line", "one liner", "tagline", "short description"], value: b.oneLiner || "" },
|
|
115
|
-
{ hints: ["what are you building", "describe your product", "about your project"], value: b.solution || "", textarea: true },
|
|
116
|
-
{ hints: ["role", "title", "position"], value: u.currentRole || "" },
|
|
117
|
-
{ hints: ["x handle", "twitter"], value: fact(user, "x handle") || fact(user, "twitter") || "" },
|
|
118
|
-
{ hints: ["linkedin"], value: u.linkedIn || "" },
|
|
119
|
-
{ hints: ["telegram"], value: fact(user, "telegram") || "" },
|
|
120
|
-
{ hints: ["github"], value: u.github || "" },
|
|
121
|
-
{ hints: ["website", "url", "product link"], value: b.website || u.portfolio || "" },
|
|
122
|
-
{ hints: ["phone", "tel"], value: u.phone || "" },
|
|
123
|
-
{ hints: ["location", "city", "where are you based"], value: u.location || b.location || "" },
|
|
124
|
-
{ hints: ["bio", "about yourself", "tell us about you"], value: u.bio || "", textarea: true },
|
|
125
|
-
{ hints: ["problem", "what problem"], value: b.problem || "", textarea: true },
|
|
126
|
-
{ hints: ["solution", "how does it work"], value: b.solution || "", textarea: true },
|
|
127
|
-
{ hints: ["traction", "progress", "metrics"], value: b.traction || "", textarea: true },
|
|
128
|
-
{ hints: ["target", "customer", "user"], value: b.targetUsers || "", textarea: true },
|
|
129
|
-
{ hints: ["business model", "revenue", "monetiz"], value: b.businessModel || "", textarea: true },
|
|
130
|
-
{ hints: ["differentiator", "competitive", "unique"], value: b.differentiators || "", textarea: true },
|
|
131
|
-
{ hints: ["why you", "right founder", "why are you"], value: u.bio || "", textarea: true },
|
|
121
|
+
{ hints: ["company", "project name", "organization"], value: ff("company") || b.productName || b.companyName || b.name || "" },
|
|
122
|
+
{ hints: ["email"], value: ff("email") || u.email || "" },
|
|
123
|
+
{ hints: ["full name", "your name", "first name"], value: ff("full name") || u.fullName || "" },
|
|
124
|
+
{ hints: ["one-line", "one liner", "tagline", "short description"], value: ff("one-liner") || b.oneLiner || "" },
|
|
125
|
+
{ hints: ["what are you building", "describe your product", "about your project"], value: ff("building") || b.solution || "", textarea: true },
|
|
126
|
+
{ hints: ["role", "title", "position"], value: ff("role") || u.currentRole || "" },
|
|
127
|
+
{ hints: ["x handle", "twitter"], value: ff("x handle") || fact(user, "x handle") || fact(user, "twitter") || "" },
|
|
128
|
+
{ hints: ["linkedin"], value: ff("linkedin") || u.linkedIn || "" },
|
|
129
|
+
{ hints: ["telegram"], value: ff("telegram") || fact(user, "telegram") || "" },
|
|
130
|
+
{ hints: ["github"], value: ff("github") || u.github || "" },
|
|
131
|
+
{ hints: ["website", "url", "product link"], value: ff("website") || b.website || u.portfolio || "" },
|
|
132
|
+
{ hints: ["phone", "tel"], value: ff("phone") || u.phone || "" },
|
|
133
|
+
{ hints: ["location", "city", "where are you based"], value: ff("location") || u.location || b.location || "" },
|
|
134
|
+
{ hints: ["bio", "about yourself", "tell us about you"], value: ff("bio") || u.bio || "", textarea: true },
|
|
135
|
+
{ hints: ["problem", "what problem"], value: ff("problem") || b.problem || "", textarea: true },
|
|
136
|
+
{ hints: ["solution", "how does it work"], value: ff("solution") || b.solution || "", textarea: true },
|
|
137
|
+
{ hints: ["traction", "progress", "metrics"], value: ff("traction") || b.traction || "", textarea: true },
|
|
138
|
+
{ hints: ["target", "customer", "user"], value: ff("target") || b.targetUsers || "", textarea: true },
|
|
139
|
+
{ hints: ["business model", "revenue", "monetiz"], value: ff("business model") || b.businessModel || "", textarea: true },
|
|
140
|
+
{ hints: ["differentiator", "competitive", "unique"], value: ff("differentiator") || b.differentiators || "", textarea: true },
|
|
141
|
+
{ hints: ["why you", "right founder", "why are you"], value: ff("right founder") || u.bio || "", textarea: true },
|
|
132
142
|
];
|
|
133
143
|
|
|
144
|
+
// Append any form persona facts that don't already map to standard fields
|
|
145
|
+
if (form?.customFacts?.length) {
|
|
146
|
+
const usedKeys = new Set(fieldMap.flatMap((f) => f.hints));
|
|
147
|
+
for (const cf of form.customFacts) {
|
|
148
|
+
if (!cf.enabled || !cf.value) continue;
|
|
149
|
+
const keyLower = cf.key.toLowerCase();
|
|
150
|
+
if ([...usedKeys].some((h) => keyLower.includes(h) || h.includes(keyLower))) continue;
|
|
151
|
+
fieldMap.push({ hints: [cf.key], value: cf.value, textarea: cf.value.length > 100 });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
134
155
|
let filled = 0, skipped = 0;
|
|
135
156
|
for (const f of fieldMap) {
|
|
136
157
|
if (!f.value) { skipped++; continue; }
|
|
@@ -146,12 +167,16 @@ export async function genericFill(page, user, biz) {
|
|
|
146
167
|
return { filled, skipped };
|
|
147
168
|
}
|
|
148
169
|
|
|
149
|
-
export async function fillForm(page, formUrl, user, biz) {
|
|
170
|
+
export async function fillForm(page, formUrl, user, biz, form) {
|
|
150
171
|
console.log(`\n Filling form...\n`);
|
|
172
|
+
if (form) {
|
|
173
|
+
const fp = form.profile || {};
|
|
174
|
+
if (fp.organization) console.log(` Form: ${fp.organization}${fp.purpose ? ` — ${fp.purpose}` : ""}`);
|
|
175
|
+
}
|
|
151
176
|
await sleep(2000);
|
|
152
177
|
|
|
153
178
|
if (formUrl.includes("nitroacc.xyz")) {
|
|
154
|
-
const data = buildNitroData(user, biz);
|
|
179
|
+
const data = buildNitroData(user, biz, form);
|
|
155
180
|
let filled = 0, skipped = 0;
|
|
156
181
|
|
|
157
182
|
for (const f of data.fields) {
|
|
@@ -180,7 +205,7 @@ export async function fillForm(page, formUrl, user, biz) {
|
|
|
180
205
|
|
|
181
206
|
console.log(`\n Filled ${filled}, skipped ${skipped} (empty persona fields).`);
|
|
182
207
|
} else {
|
|
183
|
-
const { filled, skipped } = await genericFill(page, user, biz);
|
|
208
|
+
const { filled, skipped } = await genericFill(page, user, biz, form);
|
|
184
209
|
console.log(`\n Filled ${filled}, skipped ${skipped} (empty or no match).`);
|
|
185
210
|
}
|
|
186
211
|
|
package/lib/local.js
CHANGED
|
@@ -87,6 +87,23 @@ function normalizeBusiness(raw) {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function normalizeForm(raw) {
|
|
91
|
+
if (!raw) return null;
|
|
92
|
+
const { name, facts, urls, ...rest } = raw;
|
|
93
|
+
return {
|
|
94
|
+
name: name || "unknown form",
|
|
95
|
+
urls: urls || (rest.url ? [rest.url] : []),
|
|
96
|
+
profile: {
|
|
97
|
+
organization: rest.organization || rest.org || name || "",
|
|
98
|
+
purpose: rest.purpose || "",
|
|
99
|
+
notes: rest.notes || "",
|
|
100
|
+
deadline: rest.deadline || "",
|
|
101
|
+
requirements: rest.requirements || "",
|
|
102
|
+
},
|
|
103
|
+
customFacts: factsToArray(facts),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
function factsToArray(facts) {
|
|
91
108
|
if (!facts || typeof facts !== "object") return [];
|
|
92
109
|
return Object.entries(facts).map(([key, value], i) => ({
|
|
@@ -125,7 +142,9 @@ export function loadLocalPersonas(dir) {
|
|
|
125
142
|
if (legacy) businessPersonas = [normalizeBusiness(legacy)];
|
|
126
143
|
}
|
|
127
144
|
|
|
128
|
-
|
|
145
|
+
const formPersonas = loadDir(join(dir, "forms"), normalizeForm);
|
|
146
|
+
|
|
147
|
+
return { personas, businessPersonas, formPersonas };
|
|
129
148
|
}
|
|
130
149
|
|
|
131
150
|
// ---------------------------------------------------------------------------
|
|
@@ -146,8 +165,17 @@ export function saveBusinessPersona(dir, data) {
|
|
|
146
165
|
return slug;
|
|
147
166
|
}
|
|
148
167
|
|
|
168
|
+
export function saveFormPersona(dir, data) {
|
|
169
|
+
const subdir = ensureDir(join(dir, "forms"));
|
|
170
|
+
const slug = slugify(data.name || "form");
|
|
171
|
+
writeJson(join(subdir, `${slug}.json`), data);
|
|
172
|
+
return slug;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const TYPE_DIRS = { user: "users", business: "businesses", form: "forms" };
|
|
176
|
+
|
|
149
177
|
export function deletePersonaFile(dir, type, slug) {
|
|
150
|
-
const subdir = type
|
|
178
|
+
const subdir = TYPE_DIRS[type] || type;
|
|
151
179
|
const path = join(dir, subdir, `${slug}.json`);
|
|
152
180
|
if (existsSync(path)) unlinkSync(path);
|
|
153
181
|
}
|
|
@@ -157,7 +185,7 @@ export function deletePersonaFile(dir, type, slug) {
|
|
|
157
185
|
// ---------------------------------------------------------------------------
|
|
158
186
|
|
|
159
187
|
export function listPersonaFiles(dir, type) {
|
|
160
|
-
const subdir = join(dir, type
|
|
188
|
+
const subdir = join(dir, TYPE_DIRS[type] || type);
|
|
161
189
|
if (!existsSync(subdir)) return [];
|
|
162
190
|
return readdirSync(subdir)
|
|
163
191
|
.filter((f) => f.endsWith(".json"))
|
|
@@ -215,3 +243,16 @@ export function businessTemplate() {
|
|
|
215
243
|
facts: {},
|
|
216
244
|
};
|
|
217
245
|
}
|
|
246
|
+
|
|
247
|
+
export function formTemplate() {
|
|
248
|
+
return {
|
|
249
|
+
name: "",
|
|
250
|
+
urls: [],
|
|
251
|
+
organization: "",
|
|
252
|
+
purpose: "",
|
|
253
|
+
notes: "",
|
|
254
|
+
deadline: "",
|
|
255
|
+
requirements: "",
|
|
256
|
+
facts: {},
|
|
257
|
+
};
|
|
258
|
+
}
|
package/lib/personas.js
CHANGED
|
@@ -92,7 +92,7 @@ function printPersonaSummary(label, persona, profileKeys) {
|
|
|
92
92
|
if (facts.length) console.log(` Custom facts: ${facts.length}`);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export function showPersonaDetails(user, biz) {
|
|
95
|
+
export function showPersonaDetails(user, biz, form) {
|
|
96
96
|
printPersonaSummary("User persona", user, [
|
|
97
97
|
["fullName", "Name"],
|
|
98
98
|
["email", "Email"],
|
|
@@ -108,6 +108,16 @@ export function showPersonaDetails(user, biz) {
|
|
|
108
108
|
["website", "Website"],
|
|
109
109
|
["category", "Category"],
|
|
110
110
|
]);
|
|
111
|
+
if (form) {
|
|
112
|
+
console.log(`\n Form persona: ${form.name}`);
|
|
113
|
+
const fp = form.profile || {};
|
|
114
|
+
if (fp.organization) console.log(` Organization: ${fp.organization}`);
|
|
115
|
+
if (fp.purpose) console.log(` Purpose: ${fp.purpose}`);
|
|
116
|
+
if (fp.deadline) console.log(` Deadline: ${fp.deadline}`);
|
|
117
|
+
if (fp.notes) console.log(` Notes: ${String(fp.notes).slice(0, 80)}${String(fp.notes).length > 80 ? "..." : ""}`);
|
|
118
|
+
const facts = (form.customFacts || []).filter((f) => f.enabled !== false && f.value);
|
|
119
|
+
if (facts.length) console.log(` Form-specific answers: ${facts.length}`);
|
|
120
|
+
}
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
// ---------------------------------------------------------------------------
|
|
@@ -142,6 +152,44 @@ async function pickFromList(label, list, hint, keys, defaultName) {
|
|
|
142
152
|
return list[0];
|
|
143
153
|
}
|
|
144
154
|
|
|
155
|
+
export function matchFormByUrl(formPersonas, targetUrl) {
|
|
156
|
+
if (!formPersonas?.length || !targetUrl) return null;
|
|
157
|
+
const lower = targetUrl.toLowerCase();
|
|
158
|
+
for (const fp of formPersonas) {
|
|
159
|
+
for (const u of (fp.urls || [])) {
|
|
160
|
+
if (u && lower.includes(u.toLowerCase())) return fp;
|
|
161
|
+
}
|
|
162
|
+
const orgSlug = (fp.name || "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
163
|
+
if (orgSlug && lower.includes(orgSlug)) return fp;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function selectFormPersona(formPersonas, targetUrl) {
|
|
169
|
+
if (!formPersonas?.length) return null;
|
|
170
|
+
|
|
171
|
+
// Auto-match by URL
|
|
172
|
+
const autoMatch = matchFormByUrl(formPersonas, targetUrl);
|
|
173
|
+
if (autoMatch) {
|
|
174
|
+
console.log(` Auto-matched form persona: ${autoMatch.name}`);
|
|
175
|
+
return autoMatch;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (formPersonas.length === 1) return formPersonas[0];
|
|
179
|
+
|
|
180
|
+
// Interactive
|
|
181
|
+
console.log(`\n Form personas:\n`);
|
|
182
|
+
formPersonas.forEach((f, i) => {
|
|
183
|
+
const org = f.profile?.organization || "";
|
|
184
|
+
console.log(` ${i + 1}. ${f.name}${org ? ` (${org})` : ""}`);
|
|
185
|
+
});
|
|
186
|
+
console.log(` ${formPersonas.length + 1}. (none — skip form persona)`);
|
|
187
|
+
const ans = await ask(`\n Pick form persona [1-${formPersonas.length + 1}]: `);
|
|
188
|
+
const idx = Number(ans) - 1;
|
|
189
|
+
if (idx >= 0 && idx < formPersonas.length) return formPersonas[idx];
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
145
193
|
export async function selectPersonas(dump, userHint, bizHint) {
|
|
146
194
|
const config = loadConfig();
|
|
147
195
|
const personas = dump.personas || [];
|