mirror of
https://github.com/xx254/linkedin_skills.git
synced 2026-06-10 15:34:56 +03:00
fix onboard
This commit is contained in:
@@ -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:**
|
**If user picks A or provides a file directly:**
|
||||||
Read the file they provide. If pasted inline, parse it as CSV.
|
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"].
|
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.
|
Then stop. Do not proceed until they have a file.
|
||||||
|
|
||||||
**If user picks C:**
|
**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.
|
> 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.
|
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:
|
If key columns are missing or ambiguous, do not guess. Ask for column mapping using:
|
||||||
|
|||||||
+15
-78
@@ -133,39 +133,28 @@ If C: ask for each ICP field individually, then ask for competitor description a
|
|||||||
|
|
||||||
## Step 4: Collect conversion goal
|
## Step 4: Collect conversion goal
|
||||||
|
|
||||||
Ask:
|
The user already provided a landing page URL in Step 1. Use it as the default. Ask:
|
||||||
```
|
|
||||||
What's the goal of your outreach — what action do you want an interested prospect to take?
|
|
||||||
|
|
||||||
A) Visit a landing page
|
```
|
||||||
B) Book a call
|
What do you want an interested prospect to do?
|
||||||
C) Fill out a form
|
|
||||||
D) Receive a lead magnet (e.g. guide, case study)
|
A) Visit your landing page — [URL from Step 1]
|
||||||
E) Subscribe to a newsletter
|
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
|
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
|
## Step 5: Save settings
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Write the confirmed ICP + preferences into `state/linkedin-settings.json`:
|
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.geographies`
|
||||||
- `targetMarket.alwaysExcludeRoles`
|
- `targetMarket.alwaysExcludeRoles`
|
||||||
- `qualification.disqualifyCategories` — array of snake_case labels derived in Step 2 (e.g. `["competitors", "non_decision_makers", "b2c_focused"]`)
|
- `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.type`
|
||||||
- `outreach.conversionGoal.link` (may be `null`)
|
- `outreach.conversionGoal.link` (may be `null`)
|
||||||
- `firstRunCompleted` → `true`
|
- `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)
|
## Step 6: Confirm and hand off
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
|
|
||||||
@@ -257,8 +196,6 @@ Industries: [list]
|
|||||||
Seniority: [list]
|
Seniority: [list]
|
||||||
Company size: [list]
|
Company size: [list]
|
||||||
Geographies: [list]
|
Geographies: [list]
|
||||||
Timezone: [tz]
|
|
||||||
Cadence: [frequency + time]
|
|
||||||
Goal: [conversionGoal.type] — [conversionGoal.link or "no link set"]
|
Goal: [conversionGoal.type] — [conversionGoal.link or "no link set"]
|
||||||
|
|
||||||
Settings saved to: state/linkedin-settings.json
|
Settings saved to: state/linkedin-settings.json
|
||||||
|
|||||||
Vendored
+54
-2
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const readline = require("readline");
|
||||||
const { readJson, writeJson, ensureDir } = require("./lib/io");
|
const { readJson, writeJson, ensureDir } = require("./lib/io");
|
||||||
|
|
||||||
const ROOT = path.resolve(__dirname, "..");
|
const ROOT = path.resolve(__dirname, "..");
|
||||||
@@ -62,9 +63,60 @@ if (!fs.existsSync(brandVoicePath)) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(JSON.stringify({
|
const result = {
|
||||||
ok: true,
|
ok: true,
|
||||||
settingsInitialized: !currentSettings,
|
settingsInitialized: !currentSettings,
|
||||||
stateInitialized: !currentState,
|
stateInitialized: !currentState,
|
||||||
root: ROOT
|
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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ function disqualifyCategory(record, title, company, industry, about) {
|
|||||||
const c = lower(company);
|
const c = lower(company);
|
||||||
const i = lower(industry);
|
const i = lower(industry);
|
||||||
const a = lower(about);
|
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 roleExcludes = (settings.targetMarket.alwaysExcludeRoles || []).map((v) => lower(v));
|
||||||
const companyExcludes = (settings.targetMarket.alwaysExcludeCompanyTypes || []).map((v) => lower(v));
|
const companyExcludes = (settings.targetMarket.alwaysExcludeCompanyTypes || []).map((v) => lower(v));
|
||||||
const competitorCompanyKw = (competitorProfile ? competitorProfile.companyKeywords || [] : []).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 competitorServiceKw = (competitorProfile ? competitorProfile.serviceKeywords || [] : []).map((v) => lower(v));
|
||||||
const allCompetitorCompanyKw = [...new Set([...companyExcludes, ...competitorCompanyKw])];
|
const allCompetitorCompanyKw = [...new Set([...companyExcludes, ...competitorCompanyKw])];
|
||||||
|
|
||||||
if (
|
if (activeCategories.has("non_decision_makers") && (
|
||||||
/student|intern|assistant|junior/.test(t) ||
|
/student|intern|assistant|junior/.test(t) ||
|
||||||
roleExcludes.some((r) => r && t.includes(r))
|
roleExcludes.some((r) => r && t.includes(r))
|
||||||
) {
|
)) {
|
||||||
return "non_decision_makers";
|
return "non_decision_makers";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (activeCategories.has("competitors") && (
|
||||||
allCompetitorCompanyKw.some((k) => k && c.includes(k)) ||
|
allCompetitorCompanyKw.some((k) => k && c.includes(k)) ||
|
||||||
competitorIndustryKw.some((k) => k && i.includes(k)) ||
|
competitorIndustryKw.some((k) => k && i.includes(k)) ||
|
||||||
competitorServiceKw.some((k) => k && a.includes(k))
|
competitorServiceKw.some((k) => k && a.includes(k))
|
||||||
) {
|
)) {
|
||||||
return "competitors";
|
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";
|
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";
|
return "no_outbound_intent";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"firstRunCompleted": false,
|
"firstRunCompleted": false,
|
||||||
"timezone": "America/Los_Angeles",
|
|
||||||
"cadence": {
|
|
||||||
"frequency": "daily",
|
|
||||||
"dayOfWeek": "monday",
|
|
||||||
"time": "08:00"
|
|
||||||
},
|
|
||||||
"targetMarket": {
|
"targetMarket": {
|
||||||
"industries": [],
|
"industries": [],
|
||||||
"companySizes": [],
|
"companySizes": [],
|
||||||
|
|||||||
Reference in New Issue
Block a user