From 013906e8816ef603a9c7a4057cc68275de093eb8 Mon Sep 17 00:00:00 2001 From: xx254 Date: Wed, 1 Apr 2026 22:57:02 +0800 Subject: [PATCH] fix onboard --- linkedin-lead-filter/SKILL.md | 17 ++++- onboard/SKILL.md | 93 +++++----------------------- scripts/bootstrap-system.js | 56 ++++++++++++++++- scripts/run-lead-filter.js | 15 +++-- state/linkedin-settings.example.json | 6 -- 5 files changed, 93 insertions(+), 94 deletions(-) diff --git a/linkedin-lead-filter/SKILL.md b/linkedin-lead-filter/SKILL.md index df87e93..6f0eab1 100644 --- a/linkedin-lead-filter/SKILL.md +++ b/linkedin-lead-filter/SKILL.md @@ -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: diff --git a/onboard/SKILL.md b/onboard/SKILL.md index 5d97ecd..e646821 100644 --- a/onboard/SKILL.md +++ b/onboard/SKILL.md @@ -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":"","leads":,"signals":[],"outcome":"","ts":""}' \ - 2>/dev/null & -``` - -Only run this curl if `settings.telemetry` is `"community"`. If `"anonymous"`, send `{"skill":"","ts":""}` 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 diff --git a/scripts/bootstrap-system.js b/scripts/bootstrap-system.js index 4969bb3..5703883 100644 --- a/scripts/bootstrap-system.js +++ b/scripts/bootstrap-system.js @@ -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)); +} diff --git a/scripts/run-lead-filter.js b/scripts/run-lead-filter.js index 3d7eeea..63663da 100644 --- a/scripts/run-lead-filter.js +++ b/scripts/run-lead-filter.js @@ -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"; } diff --git a/state/linkedin-settings.example.json b/state/linkedin-settings.example.json index dc4704a..c14aae4 100644 --- a/state/linkedin-settings.example.json +++ b/state/linkedin-settings.example.json @@ -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": [],