mirror of
https://github.com/xx254/linkedin_skills.git
synced 2026-06-10 15:34:56 +03:00
190 lines
5.6 KiB
JavaScript
190 lines
5.6 KiB
JavaScript
#!/usr/bin/env node
|
|
const path = require("path");
|
|
const {
|
|
readJson,
|
|
readText,
|
|
writeJson,
|
|
ensureDir,
|
|
formatDateKey,
|
|
getWeekKey
|
|
} = require("./lib/io");
|
|
|
|
const ROOT = path.resolve(__dirname, "..");
|
|
const args = process.argv.slice(2);
|
|
|
|
function getArgValue(flag) {
|
|
const index = args.findIndex((a) => a === flag);
|
|
if (index === -1 || index === args.length - 1) {
|
|
return "";
|
|
}
|
|
return args[index + 1];
|
|
}
|
|
|
|
const registryPath = getArgValue("--registry");
|
|
const leadKeyArg = getArgValue("--lead-key");
|
|
const nameArg = getArgValue("--name");
|
|
const companyArg = getArgValue("--company");
|
|
const titleArg = getArgValue("--title");
|
|
const linkedinArg = getArgValue("--linkedin");
|
|
const signalArg = getArgValue("--signal");
|
|
|
|
const settings = readJson(path.join(ROOT, "state", "linkedin-settings.json"), null);
|
|
const state = readJson(path.join(ROOT, "state", "linkedin-system-state.json"), null);
|
|
const voiceProfile = readText(path.join(ROOT, "context", "brand-voice.md"), "");
|
|
|
|
if (!settings || !state) {
|
|
console.error("Missing state files. Run: node scripts/bootstrap-system.js");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!voiceProfile.trim()) {
|
|
console.error("Missing context/brand-voice.md. Run calibrate-voice first.");
|
|
process.exit(1);
|
|
}
|
|
|
|
function lower(v) {
|
|
return String(v || "").trim().toLowerCase();
|
|
}
|
|
|
|
function canonicalLeadKey(name, company, url) {
|
|
const urlPart = String(url || "").trim() || "missing_url";
|
|
return lower(`${String(name || "").trim()}|${String(company || "").trim()}|${urlPart}`);
|
|
}
|
|
|
|
function selectLeadFromRegistry() {
|
|
if (!registryPath) return null;
|
|
const fullPath = path.resolve(process.cwd(), registryPath);
|
|
const registry = readJson(fullPath, null);
|
|
if (!registry || !Array.isArray(registry.leads)) return null;
|
|
|
|
let lead = null;
|
|
if (leadKeyArg) {
|
|
lead = registry.leads.find((item) => item.lead_key === leadKeyArg);
|
|
}
|
|
if (!lead) {
|
|
const minScore = settings.qualification.minScoreToPrioritize || 3;
|
|
lead = registry.leads.find((item) => item.qualification_status === "qualified" && item.score >= minScore);
|
|
}
|
|
if (!lead) {
|
|
lead = registry.leads.find((item) => item.qualification_status === "qualified");
|
|
}
|
|
return lead;
|
|
}
|
|
|
|
let lead = selectLeadFromRegistry();
|
|
if (!lead) {
|
|
const manualKey = canonicalLeadKey(nameArg, companyArg, linkedinArg);
|
|
if (nameArg && companyArg && titleArg) {
|
|
lead = {
|
|
lead_key: manualKey,
|
|
name: nameArg,
|
|
company: companyArg,
|
|
title: titleArg,
|
|
linkedin_url: linkedinArg || "",
|
|
signal_type: signalArg || "none",
|
|
score: 1,
|
|
qualification_status: "qualified",
|
|
reason: "manual lead input"
|
|
};
|
|
}
|
|
}
|
|
|
|
if (!lead) {
|
|
console.error("No lead found. Provide --registry or manual --name --company --title.");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (lead.qualification_status !== "qualified") {
|
|
console.error(`Lead is not qualified (${lead.qualification_status}).`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const maxChars = settings.outreach.maxChars || 300;
|
|
const maxQuestions = settings.outreach.maxQuestions || 1;
|
|
const firstMessageNoPitch = settings.outreach.firstMessageNoPitch !== false;
|
|
|
|
const recentSignal = lead.signal_type && lead.signal_type !== "none"
|
|
? lead.signal_type
|
|
: "their current role";
|
|
|
|
// Keep generation deterministic and editable in skill prompts.
|
|
function draftFromLead(record, angle = "default") {
|
|
const opener = angle === "question"
|
|
? `${record.name.split(" ")[0]}, saw your work at ${record.company} around ${recentSignal}.`
|
|
: `${record.name.split(" ")[0]}, noticed your work at ${record.company} and your focus on ${record.title}.`;
|
|
|
|
const bridge = firstMessageNoPitch
|
|
? "Curious how your team is approaching priorities this quarter."
|
|
: "I think there may be a relevant fit worth comparing.";
|
|
|
|
const question = maxQuestions > 0
|
|
? "Open to a quick exchange?"
|
|
: "";
|
|
|
|
let text = `${opener} ${bridge} ${question}`.replace(/\s+/g, " ").trim();
|
|
if (text.length > maxChars) {
|
|
text = text.slice(0, maxChars - 1).trimEnd() + ".";
|
|
}
|
|
return text;
|
|
}
|
|
|
|
const draft = draftFromLead(lead, "default");
|
|
const variant = draftFromLead(lead, "question");
|
|
|
|
const now = new Date().toISOString();
|
|
const dateKey = formatDateKey();
|
|
const outDir = path.join(ROOT, "output", `outreach_${dateKey}`);
|
|
ensureDir(outDir);
|
|
|
|
const artifact = {
|
|
lead_key: lead.lead_key,
|
|
generated_at: now,
|
|
source_registry: registryPath ? path.resolve(process.cwd(), registryPath) : "manual",
|
|
draft,
|
|
variant,
|
|
signal_used: recentSignal,
|
|
char_count: draft.length,
|
|
voice_check: "references real role/company context and keeps first-touch tone concise",
|
|
watch_for: "replace generic signal with specific post/job change when available",
|
|
status: "drafted"
|
|
};
|
|
|
|
const safeLeadKey = lead.lead_key.replace(/[^a-z0-9_|-]/gi, "_");
|
|
const artifactPath = path.join(outDir, `draft_${safeLeadKey}_${dateKey}.json`);
|
|
writeJson(artifactPath, artifact);
|
|
|
|
const weekKey = getWeekKey();
|
|
if (state.weeklyMetrics.weekKey !== weekKey) {
|
|
state.weeklyMetrics = {
|
|
weekKey,
|
|
leads_processed: 0,
|
|
qualified_count: 0,
|
|
drafted_count: 0,
|
|
reply_drafts_count: 0,
|
|
replied_count: 0,
|
|
rejected_count: 0,
|
|
score3_conversion_count: 0
|
|
};
|
|
}
|
|
|
|
state.firstRunCompleted = true;
|
|
state.lastDraftRunAt = now;
|
|
state.lastSuccessfulRunAt = now;
|
|
state.weeklyMetrics.drafted_count += 1;
|
|
state.outreachRegistry = state.outreachRegistry || {};
|
|
state.outreachRegistry[lead.lead_key] = {
|
|
lastDraftAt: now,
|
|
lastDraftStatus: "drafted",
|
|
source: artifact.source_registry,
|
|
lastSignalType: recentSignal
|
|
};
|
|
|
|
writeJson(path.join(ROOT, "state", "linkedin-system-state.json"), state);
|
|
|
|
console.log(JSON.stringify({
|
|
output: artifactPath,
|
|
lead_key: lead.lead_key,
|
|
draft_chars: draft.length,
|
|
score: lead.score
|
|
}, null, 2));
|