@1dolinski/fastforms 0.1.2 → 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 +67 -18
- package/SKILL.md +59 -21
- package/bin/fastforms.js +395 -155
- package/lib/fill.js +84 -59
- package/lib/local.js +119 -17
- package/lib/personas.js +100 -23
- package/package.json +1 -1
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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from "fs";
|
|
2
|
+
import { join, basename } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
|
|
5
5
|
export function findFastformsDir() {
|
|
@@ -25,10 +25,15 @@ function writeJson(path, data) {
|
|
|
25
25
|
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
function slugify(name) {
|
|
29
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "default";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Normalize flat local format → shape fill.js expects:
|
|
34
|
+
// { name, profile: { ...fields }, customFacts: [...] }
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
32
37
|
function normalizeUser(raw) {
|
|
33
38
|
if (!raw) return null;
|
|
34
39
|
const { name, facts, ...rest } = raw;
|
|
@@ -82,6 +87,23 @@ function normalizeBusiness(raw) {
|
|
|
82
87
|
};
|
|
83
88
|
}
|
|
84
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
|
+
|
|
85
107
|
function factsToArray(facts) {
|
|
86
108
|
if (!facts || typeof facts !== "object") return [];
|
|
87
109
|
return Object.entries(facts).map(([key, value], i) => ({
|
|
@@ -92,29 +114,92 @@ function factsToArray(facts) {
|
|
|
92
114
|
}));
|
|
93
115
|
}
|
|
94
116
|
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Multi-persona loading: reads users/ and businesses/ subdirs.
|
|
119
|
+
// Falls back to legacy user.json / business.json for backward compat.
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
function loadDir(dir, normalizer) {
|
|
123
|
+
if (!existsSync(dir)) return [];
|
|
124
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".json")).sort();
|
|
125
|
+
return files.map((f) => normalizer(readJson(join(dir, f)))).filter(Boolean);
|
|
126
|
+
}
|
|
127
|
+
|
|
95
128
|
export function loadLocalPersonas(dir) {
|
|
96
|
-
const
|
|
97
|
-
const
|
|
129
|
+
const usersDir = join(dir, "users");
|
|
130
|
+
const bizDir = join(dir, "businesses");
|
|
98
131
|
|
|
99
|
-
|
|
100
|
-
|
|
132
|
+
let personas = loadDir(usersDir, normalizeUser);
|
|
133
|
+
let businessPersonas = loadDir(bizDir, normalizeBusiness);
|
|
101
134
|
|
|
102
|
-
|
|
103
|
-
|
|
135
|
+
// Backward compat: if subdirs are empty, check legacy single files
|
|
136
|
+
if (!personas.length) {
|
|
137
|
+
const legacy = readJson(join(dir, "user.json"));
|
|
138
|
+
if (legacy) personas = [normalizeUser(legacy)];
|
|
139
|
+
}
|
|
140
|
+
if (!businessPersonas.length) {
|
|
141
|
+
const legacy = readJson(join(dir, "business.json"));
|
|
142
|
+
if (legacy) businessPersonas = [normalizeBusiness(legacy)];
|
|
143
|
+
}
|
|
104
144
|
|
|
105
|
-
|
|
145
|
+
const formPersonas = loadDir(join(dir, "forms"), normalizeForm);
|
|
146
|
+
|
|
147
|
+
return { personas, businessPersonas, formPersonas };
|
|
106
148
|
}
|
|
107
149
|
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Save personas into named files under users/ or businesses/
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
108
154
|
export function saveUserPersona(dir, data) {
|
|
109
|
-
ensureDir(dir);
|
|
110
|
-
|
|
155
|
+
const subdir = ensureDir(join(dir, "users"));
|
|
156
|
+
const slug = slugify(data.name || "default");
|
|
157
|
+
writeJson(join(subdir, `${slug}.json`), data);
|
|
158
|
+
return slug;
|
|
111
159
|
}
|
|
112
160
|
|
|
113
161
|
export function saveBusinessPersona(dir, data) {
|
|
114
|
-
ensureDir(dir);
|
|
115
|
-
|
|
162
|
+
const subdir = ensureDir(join(dir, "businesses"));
|
|
163
|
+
const slug = slugify(data.name || "default");
|
|
164
|
+
writeJson(join(subdir, `${slug}.json`), data);
|
|
165
|
+
return slug;
|
|
116
166
|
}
|
|
117
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
|
+
|
|
177
|
+
export function deletePersonaFile(dir, type, slug) {
|
|
178
|
+
const subdir = TYPE_DIRS[type] || type;
|
|
179
|
+
const path = join(dir, subdir, `${slug}.json`);
|
|
180
|
+
if (existsSync(path)) unlinkSync(path);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// List raw persona files (for edit/delete commands)
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
export function listPersonaFiles(dir, type) {
|
|
188
|
+
const subdir = join(dir, TYPE_DIRS[type] || type);
|
|
189
|
+
if (!existsSync(subdir)) return [];
|
|
190
|
+
return readdirSync(subdir)
|
|
191
|
+
.filter((f) => f.endsWith(".json"))
|
|
192
|
+
.sort()
|
|
193
|
+
.map((f) => {
|
|
194
|
+
const data = readJson(join(subdir, f));
|
|
195
|
+
return { slug: basename(f, ".json"), file: f, data };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Defaults
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
118
203
|
export function loadDefaults(dir) {
|
|
119
204
|
return readJson(join(dir, "defaults.json")) || {};
|
|
120
205
|
}
|
|
@@ -124,6 +209,10 @@ export function saveDefaults(dir, patch) {
|
|
|
124
209
|
writeJson(join(dir, "defaults.json"), { ...current, ...patch });
|
|
125
210
|
}
|
|
126
211
|
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Templates
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
127
216
|
export function userTemplate() {
|
|
128
217
|
return {
|
|
129
218
|
name: "",
|
|
@@ -154,3 +243,16 @@ export function businessTemplate() {
|
|
|
154
243
|
facts: {},
|
|
155
244
|
};
|
|
156
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,36 +108,113 @@ 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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const bizPersonas = dump.businessPersonas || [];
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Interactive persona selection — supports multiple personas from any source
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
117
126
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
async function pickFromList(label, list, hint, keys, defaultName) {
|
|
128
|
+
if (!list?.length) return null;
|
|
129
|
+
if (list.length === 1) return list[0];
|
|
121
130
|
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
// Try hint-based match
|
|
132
|
+
if (hint) {
|
|
133
|
+
const match = pickPersona(list, hint, keys);
|
|
134
|
+
if (match) return match;
|
|
135
|
+
}
|
|
124
136
|
|
|
125
|
-
//
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const ans = await ask(`\n Pick user persona [1-${personas.length}]: `);
|
|
130
|
-
const idx = Number(ans) - 1;
|
|
131
|
-
if (idx >= 0 && idx < personas.length) user = personas[idx];
|
|
137
|
+
// Try default
|
|
138
|
+
if (defaultName) {
|
|
139
|
+
const match = list.find((p) => p.name === defaultName);
|
|
140
|
+
if (match) return match;
|
|
132
141
|
}
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
// Interactive selection
|
|
144
|
+
console.log(`\n ${label}:\n`);
|
|
145
|
+
list.forEach((p, i) => {
|
|
146
|
+
const detail = keys.map((k) => k.split(".").reduce((o, s) => o?.[s], p)).filter(Boolean)[0] || "";
|
|
147
|
+
console.log(` ${i + 1}. ${p.name}${detail ? ` (${detail})` : ""}`);
|
|
148
|
+
});
|
|
149
|
+
const ans = await ask(`\n Pick ${label.toLowerCase()} [1-${list.length}]: `);
|
|
150
|
+
const idx = Number(ans) - 1;
|
|
151
|
+
if (idx >= 0 && idx < list.length) return list[idx];
|
|
152
|
+
return list[0];
|
|
153
|
+
}
|
|
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;
|
|
140
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
|
+
|
|
193
|
+
export async function selectPersonas(dump, userHint, bizHint) {
|
|
194
|
+
const config = loadConfig();
|
|
195
|
+
const personas = dump.personas || [];
|
|
196
|
+
const bizPersonas = dump.businessPersonas || [];
|
|
197
|
+
|
|
198
|
+
const effectiveUserHint = userHint || "";
|
|
199
|
+
const effectiveBizHint = bizHint || "";
|
|
200
|
+
const defaultUser = config.defaultUser || "";
|
|
201
|
+
const defaultBiz = config.defaultBusiness || "";
|
|
202
|
+
|
|
203
|
+
const user = await pickFromList(
|
|
204
|
+
"User personas",
|
|
205
|
+
personas,
|
|
206
|
+
effectiveUserHint,
|
|
207
|
+
["name", "profile.fullName"],
|
|
208
|
+
defaultUser,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const biz = await pickFromList(
|
|
212
|
+
"Business personas",
|
|
213
|
+
bizPersonas,
|
|
214
|
+
effectiveBizHint,
|
|
215
|
+
["name", "profile.companyName", "profile.productName"],
|
|
216
|
+
defaultBiz,
|
|
217
|
+
);
|
|
141
218
|
|
|
142
219
|
return { user, biz };
|
|
143
220
|
}
|