fix onboard

This commit is contained in:
xx254
2026-04-01 22:57:02 +08:00
parent 743a87d9cf
commit 013906e881
5 changed files with 93 additions and 94 deletions
+15 -2
View File
@@ -77,7 +77,8 @@ C) I don't have a CSV — show me how signals work
**If user picks A or provides a file directly:**
Read the file they provide. If pasted inline, parse it as CSV.
Acknowledge the leads briefly, then show the signal showcase before proceeding:
Acknowledge the leads briefly, then show the signal showcase before proceeding.
(Only show the btw block for option A — skip it for option C.)
```
Got it — [N] leads loaded from [filename or "your CSV"].
@@ -136,9 +137,21 @@ Say:
Then stop. Do not proceed until they have a file.
**If user picks C:**
Read `sample_warm_leads.example.csv` from the repo root and proceed with that as the input. Tell the user:
Read `sample_warm_leads.example.csv` from the repo root. Tell the user:
> Loading sample warm leads — placeholder contacts with realistic signals so you can run the full pipeline right now. Swap in your real leads any time.
Do **not** show the btw/signals table.
Display the sample leads as a readable table (Name, Title, Company, Signal columns). Then ask:
```
Here are your sample leads. Want to change anything before I run the filter?
(edit a row, swap someone out, add a lead — or just say "looks good")
```
If they request changes, apply them, re-display the updated table, and ask again.
Once they confirm ("looks good" or equivalent), proceed with the (possibly edited) leads as the input.
Identify the columns available. At minimum, look for: Name, Title, Company, LinkedIn URL, and any signal or engagement data.
If key columns are missing or ambiguous, do not guess. Ask for column mapping using:
+15 -78
View File
@@ -133,39 +133,28 @@ If C: ask for each ICP field individually, then ask for competitor description a
## Step 4: Collect conversion goal
Ask:
```
What's the goal of your outreach — what action do you want an interested prospect to take?
The user already provided a landing page URL in Step 1. Use it as the default. Ask:
A) Visit a landing page
B) Book a call
C) Fill out a form
D) Receive a lead magnet (e.g. guide, case study)
E) Subscribe to a newsletter
```
What do you want an interested prospect to do?
A) Visit your landing page — [URL from Step 1]
B) Book a call — paste a Calendly or booking link
C) Fill out a form — paste the form URL
D) Receive a lead magnet — paste the link
E) Subscribe to a newsletter — paste the link
F) Other — describe it
If you have a link for it, paste it now. You can also skip this and add it later.
```
Save the choice as `outreach.conversionGoal.type` and the link (if provided) as `outreach.conversionGoal.link`. If no link is given, leave `link` as `null`. Do not block progress if the link is missing.
Wait for the user to reply before continuing. Do not assume A.
If user confirms A: set `outreach.conversionGoal.type` to `landing_page` and `outreach.conversionGoal.link` to the URL from Step 1.
Otherwise: save their choice as type and their link as the URL. If no link given, leave `link` as `null`. Do not block progress.
---
## Step 5: Collect timezone and cadence
Ask:
```
Two quick preferences:
1. Your timezone? (e.g. America/New_York, Europe/London, Asia/Singapore)
2. How often do you want to run outreach?
A) Daily — pick a time (e.g. 08:00)
B) Weekly — pick a day + time (e.g. Monday 08:00)
```
---
## Step 6: Save settings
## Step 5: Save settings
Write the confirmed ICP + preferences into `state/linkedin-settings.json`:
@@ -175,8 +164,6 @@ Write the confirmed ICP + preferences into `state/linkedin-settings.json`:
- `targetMarket.geographies`
- `targetMarket.alwaysExcludeRoles`
- `qualification.disqualifyCategories` — array of snake_case labels derived in Step 2 (e.g. `["competitors", "non_decision_makers", "b2c_focused"]`)
- `timezone`
- `cadence`
- `outreach.conversionGoal.type`
- `outreach.conversionGoal.link` (may be `null`)
- `firstRunCompleted``true`
@@ -197,55 +184,7 @@ Also write the competitor profile to `state/competitor-profile.json`:
---
## Step 6.5: Telemetry opt-in (one time only)
Check if `state/linkedin-system-state.json` has `"telemetryPrompted": true`. If yes, skip this step entirely.
Ask once using AskUserQuestion:
> Help us build better signals for your industry! Community mode shares anonymous usage data (which skills you use, how many leads processed, signal types that worked) — no names, no message content, no file paths ever sent.
> Change anytime by setting `"telemetry": "off"` in `state/linkedin-settings.json`.
Options:
- A) Sure, happy to help improve this
- B) No thanks
If A: set `settings.telemetry = "community"` in `state/linkedin-settings.json`
If B: ask one follow-up:
> How about anonymous mode? We just learn that *someone* ran the pipeline — no ID, no details. Just a counter that helps us know the tool is useful.
Options:
- A) Anonymous is fine
- B) No thanks, fully off
If B→A: set `settings.telemetry = "anonymous"`
If B→B: set `settings.telemetry = "off"`
Always set `state.telemetryPrompted = true` in `state/linkedin-system-state.json` after this step regardless of choice.
**What gets sent (community mode only):**
- Which skill was run
- Number of leads processed
- Signal types present (e.g. `post_like`, `funding_news`) — no names or content
- Outcome (done / blocked / needs_context)
- Timestamp
**What is never sent:** names, companies, LinkedIn URLs, message content, file paths, your landing page URL.
**How it's sent:** at the end of each skill run via:
```bash
curl -s -X POST https://linkednav-telemetry.vercel.app/api/telemetry \
-H "Content-Type: application/json" \
-d '{"skill":"<skill_name>","leads":<count>,"signals":[<types>],"outcome":"<outcome>","ts":"<iso>"}' \
2>/dev/null &
```
Only run this curl if `settings.telemetry` is `"community"`. If `"anonymous"`, send `{"skill":"<skill_name>","ts":"<iso>"}` only. If `"off"`, skip entirely.
---
## Step 7: Confirm and hand off
## Step 6: Confirm and hand off
Output:
@@ -257,8 +196,6 @@ Industries: [list]
Seniority: [list]
Company size: [list]
Geographies: [list]
Timezone: [tz]
Cadence: [frequency + time]
Goal: [conversionGoal.type] — [conversionGoal.link or "no link set"]
Settings saved to: state/linkedin-settings.json
+54 -2
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const { readJson, writeJson, ensureDir } = require("./lib/io");
const ROOT = path.resolve(__dirname, "..");
@@ -62,9 +63,60 @@ if (!fs.existsSync(brandVoicePath)) {
);
}
console.log(JSON.stringify({
const result = {
ok: true,
settingsInitialized: !currentSettings,
stateInitialized: !currentState,
root: ROOT
}, null, 2));
};
// Telemetry opt-in — ask once on first setup
const freshState = readJson(statePath, {});
if (!freshState.telemetryPrompted) {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
(async () => {
console.log("\n──────────────────────────────────────────");
console.log("Help us improve these skills for your industry.");
console.log("Community mode shares anonymous usage data — which skills you run,");
console.log("how many leads processed, signal types that worked.");
console.log("No names, no message content, no file paths. Ever.");
console.log("Change anytime: set \"telemetry\": \"off\" in state/linkedin-settings.json");
console.log("──────────────────────────────────────────");
const a1 = await ask("\nA) Sure, happy to help improve this\nB) No thanks\n> ");
let telemetryLevel;
if (a1.trim().toUpperCase() === "A") {
telemetryLevel = "community";
} else {
const a2 = await ask(
"\nHow about anonymous mode? We just learn that someone ran the pipeline —\n" +
"no ID, no details. Just a counter that helps us know the tool is useful.\n\n" +
"A) Anonymous is fine\nB) No thanks, fully off\n> "
);
telemetryLevel = a2.trim().toUpperCase() === "A" ? "anonymous" : "off";
}
rl.close();
// Save telemetry choice to settings
const settings = readJson(settingsPath, {});
if (!settings.settings) settings.settings = {};
settings.settings.telemetry = telemetryLevel;
writeJson(settingsPath, settings);
// Mark telemetry as prompted in state
const state = readJson(statePath, {});
state.telemetryPrompted = true;
writeJson(statePath, state);
result.telemetry = telemetryLevel;
console.log("\n" + JSON.stringify(result, null, 2));
})();
} else {
console.log(JSON.stringify(result, null, 2));
}
+9 -6
View File
@@ -103,6 +103,7 @@ function disqualifyCategory(record, title, company, industry, about) {
const c = lower(company);
const i = lower(industry);
const a = lower(about);
const activeCategories = new Set((settings.qualification.disqualifyCategories || []).map((v) => lower(v)));
const roleExcludes = (settings.targetMarket.alwaysExcludeRoles || []).map((v) => lower(v));
const companyExcludes = (settings.targetMarket.alwaysExcludeCompanyTypes || []).map((v) => lower(v));
const competitorCompanyKw = (competitorProfile ? competitorProfile.companyKeywords || [] : []).map((v) => lower(v));
@@ -110,26 +111,28 @@ function disqualifyCategory(record, title, company, industry, about) {
const competitorServiceKw = (competitorProfile ? competitorProfile.serviceKeywords || [] : []).map((v) => lower(v));
const allCompetitorCompanyKw = [...new Set([...companyExcludes, ...competitorCompanyKw])];
if (
if (activeCategories.has("non_decision_makers") && (
/student|intern|assistant|junior/.test(t) ||
roleExcludes.some((r) => r && t.includes(r))
) {
)) {
return "non_decision_makers";
}
if (
if (activeCategories.has("competitors") && (
allCompetitorCompanyKw.some((k) => k && c.includes(k)) ||
competitorIndustryKw.some((k) => k && i.includes(k)) ||
competitorServiceKw.some((k) => k && a.includes(k))
) {
)) {
return "competitors";
}
if (/consumer|retail|b2c/.test(i) && !/b2b|software|saas|services/.test(i)) {
if (activeCategories.has("b2c_focused") &&
/consumer|retail|b2c/.test(i) && !/b2b|software|saas|services/.test(i)) {
return "b2c_focused";
}
if (/engineer|developer|support|it admin|back office/.test(t) && !/head|director|vp|chief/.test(t)) {
if (activeCategories.has("no_outbound_intent") &&
/engineer|developer|support|it admin|back office/.test(t) && !/head|director|vp|chief/.test(t)) {
return "no_outbound_intent";
}
-6
View File
@@ -1,12 +1,6 @@
{
"version": "1.0.0",
"firstRunCompleted": false,
"timezone": "America/Los_Angeles",
"cadence": {
"frequency": "daily",
"dayOfWeek": "monday",
"time": "08:00"
},
"targetMarket": {
"industries": [],
"companySizes": [],