@sonnechasser/ntrp 1.4.9 → 1.5.1

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.
Files changed (3) hide show
  1. package/dist/index.js +729 -166
  2. package/dist/mcp/server.js +1116 -993
  3. package/package.json +2 -1
@@ -6026,6 +6026,10 @@ function stripTools(req) {
6026
6026
  return rest;
6027
6027
  }
6028
6028
  async function outboundRequest(req) {
6029
+ if (req.skipPseudonymize) {
6030
+ const { skipPseudonymize: _drop, ...rest } = req;
6031
+ return rest;
6032
+ }
6029
6033
  await ensureLexiconSeeded();
6030
6034
  return protectRequest(req);
6031
6035
  }
@@ -6287,13 +6291,14 @@ var init_failover = __esm({
6287
6291
  });
6288
6292
 
6289
6293
  // src/ai/llm/complete.ts
6290
- async function llmCompleteText(surface, system, userMessage, max_tokens, ctx) {
6294
+ async function llmCompleteText(surface, system, userMessage, max_tokens, ctx, opts = {}) {
6291
6295
  const { response, meta } = await completeWithFailover(
6292
6296
  {
6293
6297
  surface,
6294
6298
  system,
6295
6299
  messages: [{ role: "user", content: userMessage }],
6296
- max_tokens
6300
+ max_tokens,
6301
+ ...opts.skipPseudonymize ? { skipPseudonymize: true } : {}
6297
6302
  },
6298
6303
  { ctx }
6299
6304
  );
@@ -9975,10 +9980,9 @@ section: Settings
9975
9980
  handler: ../commands/onboard.ts
9976
9981
  ---
9977
9982
 
9978
- Start the first-run wizard. It builds a company profile.
9979
- Connect one or two keys (Anthropic, OpenAI, or another provider).
9980
- Then answer a few seed questions. AI drafts industry, ICP, deal size, and stack guesses.
9981
- Optional last step: pick a folder for desktop-AI handoffs if it is not already set. Skip, and type \`/inbox set\` later \u2014 NTRP will not ask again.
9983
+ Start the progressive setup ladder. Each \`/onboard\` runs the next incomplete tier:
9984
+ profile (keyless) \u2192 domain research + API key \u2192 sample demo data \u2192 production CSV/folder path.
9985
+ Drag-drop a CSV or folder into the REPL anytime for real data.
9982
9986
  The profile is stored at \`~/.ntrp/profile.json\`. It flows into findings, NL answers, and demo data.`
9983
9987
  },
9984
9988
  {
@@ -20838,724 +20842,985 @@ var init_demo = __esm({
20838
20842
  }
20839
20843
  });
20840
20844
 
20841
- // src/pipeline/csv-parse.ts
20842
- var csv_parse_exports = {};
20843
- __export(csv_parse_exports, {
20844
- getPreviewRows: () => getPreviewRows,
20845
- parseCSV: () => parseCSV
20845
+ // src/demo/scenarios.ts
20846
+ var scenarios_exports = {};
20847
+ __export(scenarios_exports, {
20848
+ NAMED_DEMO_SCENARIOS: () => NAMED_DEMO_SCENARIOS,
20849
+ SCENARIOS: () => SCENARIOS,
20850
+ SCENARIO_LIST: () => SCENARIO_LIST,
20851
+ blendScenarios: () => blendScenarios,
20852
+ getScenario: () => getScenario,
20853
+ isNamedDemoScenario: () => isNamedDemoScenario,
20854
+ pickRandomScenario: () => pickRandomScenario,
20855
+ resolveScenarioInput: () => resolveScenarioInput
20846
20856
  });
20847
- import Papa from "papaparse";
20848
- function parseCSV(content) {
20849
- const result = Papa.parse(content, {
20850
- header: true,
20851
- skipEmptyLines: true,
20852
- transformHeader: (h) => h.trim()
20853
- });
20854
- return {
20855
- headers: result.meta.fields ?? [],
20856
- rows: result.data,
20857
- rowCount: result.data.length
20858
- };
20857
+ function getScenario(key) {
20858
+ const scenario = SCENARIOS[key];
20859
+ if (!scenario) {
20860
+ throw new Error(`Unknown scenario: ${key}. Valid: ${Object.keys(SCENARIOS).join(", ")}`);
20861
+ }
20862
+ return scenario;
20859
20863
  }
20860
- function getPreviewRows(rows, count = 5) {
20861
- return rows.slice(0, count);
20864
+ function isNamedDemoScenario(raw) {
20865
+ return NAMED_DEMO_SCENARIOS.includes(raw);
20862
20866
  }
20863
- var init_csv_parse = __esm({
20864
- "src/pipeline/csv-parse.ts"() {
20865
- "use strict";
20867
+ function resolveScenarioInput(raw) {
20868
+ const input = raw?.trim();
20869
+ if (!input) return void 0;
20870
+ if (input === "research_blend") return "research_blend";
20871
+ if (NAMED_DEMO_SCENARIOS.includes(input)) {
20872
+ return input;
20866
20873
  }
20867
- });
20868
-
20869
- // src/pipeline/csv-mappings.ts
20870
- function getDefaultMappings(sourceSystem, entityType) {
20871
- return DEFAULT_MAPPINGS[sourceSystem]?.[entityType] ?? {};
20874
+ const n = Number(input);
20875
+ if (Number.isInteger(n) && n >= 1 && n <= NAMED_DEMO_SCENARIOS.length) {
20876
+ return NAMED_DEMO_SCENARIOS[n - 1];
20877
+ }
20878
+ return null;
20872
20879
  }
20873
- var DEFAULT_MAPPINGS, ACTIVITY_TYPE_MAP, CAMPAIGN_TYPE_MAP;
20874
- var init_csv_mappings = __esm({
20875
- "src/pipeline/csv-mappings.ts"() {
20880
+ function pickRandomScenario() {
20881
+ return RANDOM_POOL[Math.floor(Math.random() * RANDOM_POOL.length)];
20882
+ }
20883
+ function blendScenarios(_research) {
20884
+ const blended = {
20885
+ ...BASELINE,
20886
+ key: "research_blend",
20887
+ label: "Research-Derived Blend",
20888
+ description: "Realistic data with mild-to-moderate problems across all vital signs.",
20889
+ story: "Generated with default research blend. Problems are seeded across all vital signs at realistic levels.",
20890
+ hook: "Mild-to-moderate problems seeded across all five vitals.",
20891
+ // Bump all problems slightly above baseline for discoverability
20892
+ staleContactRatio: 0.2,
20893
+ pastCloseDateRatio: 0.15,
20894
+ mqlDropRatio: 0.15,
20895
+ qualifiedNoOutreachRatio: 0.15,
20896
+ stuckDealRatio: 0.15,
20897
+ noiseActivityRatio: 0.2,
20898
+ singleThreadRatio: 0.25
20899
+ };
20900
+ return blended;
20901
+ }
20902
+ var BASELINE, SCENARIOS, SCENARIO_LIST, NAMED_DEMO_SCENARIOS, RANDOM_POOL;
20903
+ var init_scenarios = __esm({
20904
+ "src/demo/scenarios.ts"() {
20876
20905
  "use strict";
20877
- DEFAULT_MAPPINGS = {
20878
- salesforce: {
20879
- organizations: { Name: "canonical_name", Website: "canonical_domain", Id: "source_id" },
20880
- people: { Email: "canonical_email", FirstName: "first_name", LastName: "last_name", Id: "source_id", AccountId: "organization_source_id" },
20881
- opportunities: { Name: "canonical_name", Amount: "amount", StageName: "current_stage", CloseDate: "close_date", Id: "source_id", AccountId: "organization_source_id", OwnerId: "owner_source_id" },
20882
- activities: { TaskSubtype: "activity_type", ActivityDate: "occurred_at", Id: "source_id", WhoId: "person_source_id" }
20906
+ BASELINE = {
20907
+ enterpriseRatio: 0.2,
20908
+ midMarketRatio: 0.3,
20909
+ smbRatio: 0.5,
20910
+ staleContactRatio: 0.15,
20911
+ staleContactRatioEnterprise: 0.2,
20912
+ staleContactRatioSmb: 0.1,
20913
+ pastCloseDateRatio: 0.1,
20914
+ staleDays: 120,
20915
+ mqlDropRatio: 0.1,
20916
+ qualifiedNoOutreachRatio: 0.1,
20917
+ stuckDealRatio: 0.1,
20918
+ stuckInNegotiationDays: 45,
20919
+ avgDaysPerStageEnterprise: 25,
20920
+ avgDaysPerStageSmb: 8,
20921
+ activityVolumeMultiplier: 1,
20922
+ noiseActivityRatio: 0.15,
20923
+ singleThreadRatio: 0.2,
20924
+ loneWolfRepIndex: null,
20925
+ loneWolfSingleThreadRatio: 0,
20926
+ freshnessGapDays: 90
20927
+ };
20928
+ SCENARIOS = {
20929
+ hidden_crisis: {
20930
+ ...BASELINE,
20931
+ key: "hidden_crisis",
20932
+ label: "The Hidden Crisis",
20933
+ description: "Overall health looks yellow but Enterprise is deep red, masked by strong SMB numbers.",
20934
+ story: "Your aggregate numbers look okay \u2014 but when you break it by segment, Enterprise is dying. 60% of enterprise contacts have gone dark, deals are single-threaded, and SMB is carrying the average.",
20935
+ hook: "SMB is carrying the average while Enterprise dies quietly.",
20936
+ staleContactRatio: 0.3,
20937
+ staleContactRatioEnterprise: 0.6,
20938
+ staleContactRatioSmb: 0.1,
20939
+ singleThreadRatio: 0.5,
20940
+ enterpriseRatio: 0.3,
20941
+ midMarketRatio: 0.3,
20942
+ smbRatio: 0.4
20883
20943
  },
20884
- hubspot: {
20885
- organizations: { "Company name": "canonical_name", "Company Domain Name": "canonical_domain", "Company ID": "source_id" },
20886
- people: { Email: "canonical_email", "First Name": "first_name", "Last Name": "last_name", "Contact ID": "source_id", "Associated Company ID": "organization_source_id" },
20887
- activities: { Type: "activity_type", "Created At": "occurred_at", "Engagement ID": "source_id", "Contact ID": "person_source_id" }
20944
+ leaky_bucket: {
20945
+ ...BASELINE,
20946
+ key: "leaky_bucket",
20947
+ label: "The Leaky Bucket",
20948
+ description: "Marketing generates plenty of leads but 40% vanish at handoff to sales.",
20949
+ story: "Marketing is doing its job \u2014 MQLs are flowing. But 40% of qualified leads never show up in sales workflows. They're falling through the cracks at handoff, and nobody's noticing because marketing reports MQL count and sales reports pipeline value.",
20950
+ hook: "MQLs flow in, then 40% vanish at the sales handoff.",
20951
+ mqlDropRatio: 0.4,
20952
+ qualifiedNoOutreachRatio: 0.35,
20953
+ staleContactRatio: 0.2
20888
20954
  },
20889
- outreach: {
20890
- campaigns: { name: "canonical_name", id: "source_id" },
20891
- activities: { type: "activity_type", created_at: "occurred_at", id: "source_id", prospect_id: "person_source_id" }
20955
+ stale_pipeline: {
20956
+ ...BASELINE,
20957
+ key: "stale_pipeline",
20958
+ label: "The Stale Pipeline",
20959
+ description: "Big pipeline number but half the deals are zombies stuck in late stages.",
20960
+ story: "The pipeline report says $5M. But look closer: half those deals have close dates in the past, 40% are stuck in Negotiation for 120+ days, and nobody's touching them. You're forecasting on fiction.",
20961
+ hook: "Half the pipeline is zombies \u2014 you're forecasting on fiction.",
20962
+ pastCloseDateRatio: 0.5,
20963
+ stuckDealRatio: 0.4,
20964
+ stuckInNegotiationDays: 120,
20965
+ staleContactRatio: 0.25,
20966
+ staleDays: 90
20967
+ },
20968
+ lone_wolf: {
20969
+ ...BASELINE,
20970
+ key: "lone_wolf",
20971
+ label: "The Lone Wolf",
20972
+ description: "One rep has great numbers but every single deal is single-threaded.",
20973
+ story: "Your top rep is crushing it on paper \u2014 biggest pipeline, highest close rate. But every deal has exactly one contact. One champion goes on vacation, gets promoted, or leaves, and the entire pipeline collapses.",
20974
+ hook: "Top rep, huge pipeline, one contact per deal \u2014 one exit from collapse.",
20975
+ loneWolfRepIndex: 0,
20976
+ loneWolfSingleThreadRatio: 1,
20977
+ singleThreadRatio: 0.15
20978
+ },
20979
+ busy_bees: {
20980
+ ...BASELINE,
20981
+ key: "busy_bees",
20982
+ label: "The Busy Bees",
20983
+ description: "High activity volume across the team, but most of it hits dead ends.",
20984
+ story: "Your team is busy. Activity metrics look great \u2014 calls are up, emails are up, meetings are up. But 60% of that activity is aimed at contacts with no associated pipeline. Reps are spraying, not aiming.",
20985
+ hook: "Reps are spraying, not aiming.",
20986
+ activityVolumeMultiplier: 3,
20987
+ noiseActivityRatio: 0.6,
20988
+ staleContactRatio: 0.2
20989
+ },
20990
+ even_keel: {
20991
+ ...BASELINE,
20992
+ key: "even_keel",
20993
+ label: "The Even Keel",
20994
+ description: "A reasonably healthy book \u2014 enough yellow to listen, not a five-alarm fire.",
20995
+ story: "Most numbers sit in a normal band. A few contacts have gone quiet, a handful of deals are slow, activity is mostly on-pipeline. This is what 'fine' looks like on the stethoscope \u2014 useful when you want to evaluate NTRP without a manufactured crisis.",
20996
+ hook: "Reasonably healthy \u2014 enough signal to listen, not a crisis."
20997
+ },
20998
+ compound_pain: {
20999
+ ...BASELINE,
21000
+ key: "compound_pain",
21001
+ label: "The Compound Fracture",
21002
+ description: "Several vitals are red at once \u2014 stale pipeline, leaky handoff, noisy activity, thin threads.",
21003
+ story: "This isn't one problem. Enterprise contacts have gone dark, MQLs vanish at handoff, late-stage deals are zombies, and a lot of activity never touches pipeline. The gating logic has to pick a first red \u2014 that's the point of this book.",
21004
+ hook: "Several vitals red at once \u2014 the stethoscope has to pick a first listen.",
21005
+ enterpriseRatio: 0.3,
21006
+ midMarketRatio: 0.3,
21007
+ smbRatio: 0.4,
21008
+ staleContactRatio: 0.35,
21009
+ staleContactRatioEnterprise: 0.55,
21010
+ staleContactRatioSmb: 0.15,
21011
+ pastCloseDateRatio: 0.35,
21012
+ staleDays: 100,
21013
+ mqlDropRatio: 0.3,
21014
+ qualifiedNoOutreachRatio: 0.25,
21015
+ stuckDealRatio: 0.3,
21016
+ stuckInNegotiationDays: 90,
21017
+ activityVolumeMultiplier: 2,
21018
+ noiseActivityRatio: 0.4,
21019
+ singleThreadRatio: 0.4
20892
21020
  }
20893
21021
  };
20894
- ACTIVITY_TYPE_MAP = {
20895
- Call: "call",
20896
- Email: "email",
20897
- Task: "custom",
20898
- CALL: "call",
20899
- EMAIL: "email",
20900
- MEETING: "meeting",
20901
- NOTE: "custom",
20902
- TASK: "custom",
20903
- INCOMING_EMAIL: "email",
20904
- FORWARDED_EMAIL: "email",
20905
- call: "call",
20906
- email: "email",
20907
- sequence_step: "email"
20908
- };
20909
- CAMPAIGN_TYPE_MAP = {
20910
- outbound: "email_sequence",
20911
- inbound: "email_sequence",
20912
- trigger: "email_sequence"
20913
- };
21022
+ SCENARIO_LIST = Object.values(SCENARIOS);
21023
+ NAMED_DEMO_SCENARIOS = [
21024
+ "hidden_crisis",
21025
+ "leaky_bucket",
21026
+ "stale_pipeline",
21027
+ "lone_wolf",
21028
+ "busy_bees",
21029
+ "even_keel",
21030
+ "compound_pain"
21031
+ ];
21032
+ RANDOM_POOL = [...NAMED_DEMO_SCENARIOS];
20914
21033
  }
20915
21034
  });
20916
21035
 
20917
- // src/pipeline/csv-detect.ts
20918
- var csv_detect_exports = {};
20919
- __export(csv_detect_exports, {
20920
- detectEntityType: () => detectEntityType
20921
- });
20922
- function detectEntityType(headers, sourceSystem) {
20923
- const rules = SIGNATURES[sourceSystem];
20924
- if (!rules) return null;
20925
- const headerSet = new Set(headers);
20926
- let bestMatch = null;
20927
- for (const rule of rules) {
20928
- const requiredMatches = rule.required.filter((col) => headerSet.has(col)).length;
20929
- const requiredTotal = rule.required.length;
20930
- if (requiredMatches < Math.ceil(requiredTotal / 2)) continue;
20931
- const optionalMatches = rule.optional.filter((col) => headerSet.has(col)).length;
20932
- const optionalTotal = rule.optional.length;
20933
- const score = requiredMatches / requiredTotal * 0.7 + (optionalTotal > 0 ? optionalMatches / optionalTotal * 0.3 : 0.3);
20934
- if (!bestMatch || score > bestMatch.score) {
20935
- bestMatch = { rule, score };
20936
- }
20937
- }
20938
- if (!bestMatch) return null;
20939
- const defaultMappings = getDefaultMappings(sourceSystem, bestMatch.rule.entityType);
20940
- const mappings = {};
20941
- for (const [csvCol, ontologyField] of Object.entries(defaultMappings)) {
20942
- if (headerSet.has(csvCol)) {
20943
- mappings[csvCol] = ontologyField;
20944
- }
20945
- }
21036
+ // src/demo/scenario-fit.ts
21037
+ function dealBandFromCycleDays(days) {
21038
+ if (days == null || !Number.isFinite(days) || days <= 0) return void 0;
21039
+ if (days <= 21) return "velocity";
21040
+ if (days <= 45) return "core";
21041
+ if (days <= 90) return "mid";
21042
+ return "enterprise";
21043
+ }
21044
+ function dealBandFromAverageDealSize(raw) {
21045
+ if (!raw) return void 0;
21046
+ const t = raw.trim().toLowerCase();
21047
+ if (!t) return void 0;
21048
+ const match = t.match(/(\d+(?:\.\d+)?)\s*(k|m|million|thousand)?/i);
21049
+ if (!match) return void 0;
21050
+ let n = Number(match[1]);
21051
+ if (!Number.isFinite(n)) return void 0;
21052
+ const unit = (match[2] ?? "").toLowerCase();
21053
+ if (unit === "k" || unit === "thousand") n *= 1e3;
21054
+ else if (unit === "m" || unit === "million") n *= 1e6;
21055
+ else if (n > 0 && n < 500) n *= 1e3;
21056
+ if (n < 15e3) return "velocity";
21057
+ if (n < 5e4) return "core";
21058
+ if (n < 15e4) return "mid";
21059
+ return "enterprise";
21060
+ }
21061
+ function signalsFromProfile(profile) {
21062
+ if (!profile) return {};
21063
+ const text = [
21064
+ profile.industry,
21065
+ profile.product_description,
21066
+ profile.target_customer,
21067
+ profile.user_scope,
21068
+ profile.custom_context
21069
+ ].filter(Boolean).join("\n");
20946
21070
  return {
20947
- entityType: bestMatch.rule.entityType,
20948
- confidence: Math.round(bestMatch.score * 100) / 100,
20949
- mappings
21071
+ salesMotion: profile.sales_motion,
21072
+ dealBand: dealBandFromAverageDealSize(profile.average_deal_size) ?? dealBandFromCycleDays(profile.sales_cycle_days),
21073
+ cycleDays: profile.sales_cycle_days,
21074
+ text
20950
21075
  };
20951
21076
  }
20952
- var SIGNATURES;
20953
- var init_csv_detect = __esm({
20954
- "src/pipeline/csv-detect.ts"() {
20955
- "use strict";
20956
- init_csv_mappings();
20957
- SIGNATURES = {
20958
- salesforce: [
20959
- { entityType: "organizations", required: ["Name", "Industry"], optional: ["AnnualRevenue", "BillingCountry", "Website", "NumberOfEmployees"], weight: 1 },
20960
- { entityType: "people", required: ["FirstName", "LastName", "Email"], optional: ["AccountId", "Title", "Phone"], weight: 1 },
20961
- { entityType: "opportunities", required: ["StageName", "Amount", "CloseDate"], optional: ["AccountId", "OwnerId", "Probability"], weight: 1 },
20962
- { entityType: "activities", required: ["Subject", "ActivityDate", "TaskSubtype"], optional: ["WhoId", "WhatId", "Status"], weight: 1 }
20963
- ],
20964
- hubspot: [
20965
- { entityType: "organizations", required: ["Company name", "Company Domain Name"], optional: ["Company ID", "Industry", "Annual Revenue"], weight: 1 },
20966
- { entityType: "people", required: ["First Name", "Last Name", "Email"], optional: ["Contact ID", "Associated Company ID", "Lifecycle Stage"], weight: 1 },
20967
- { entityType: "activities", required: ["Engagement ID", "Type"], optional: ["Timestamp", "Created At", "Contact ID"], weight: 1 }
20968
- ],
20969
- outreach: [
20970
- { entityType: "campaigns", required: ["sequence_type", "step_count"], optional: ["name", "id", "enabled"], weight: 1 },
20971
- { entityType: "activities", required: ["prospect_id", "prospect_email"], optional: ["type", "created_at", "subject"], weight: 1 }
20972
- ]
20973
- };
20974
- }
20975
- });
20976
-
20977
- // src/pipeline/importer.ts
20978
- async function importRows(params) {
20979
- let result;
20980
- switch (params.entityType) {
20981
- case "organizations":
20982
- result = await importOrganizations(params);
20983
- break;
20984
- case "people":
20985
- result = await importPeople(params);
20986
- break;
20987
- case "opportunities":
20988
- result = await importOpportunities(params);
20989
- break;
20990
- case "activities":
20991
- result = await importActivities(params);
20992
- break;
20993
- case "campaigns":
20994
- result = await importCampaigns(params);
20995
- break;
20996
- default:
20997
- result = { imported: 0, errors: [`Unknown entity type: ${params.entityType}`] };
20998
- }
20999
- const { invalidateLexiconSeed: invalidateLexiconSeed2 } = await Promise.resolve().then(() => (init_lexicon_seed(), lexicon_seed_exports));
21000
- invalidateLexiconSeed2();
21001
- return result;
21002
- }
21003
- function reverseMap(mappings) {
21004
- const reversed = {};
21005
- for (const [csvCol, ontologyField] of Object.entries(mappings)) {
21006
- reversed[ontologyField] = csvCol;
21077
+ function keywordHits(text) {
21078
+ const hits = {};
21079
+ const hay = text.toLowerCase();
21080
+ for (const { scenario, patterns } of KEYWORD_HINTS) {
21081
+ let n = 0;
21082
+ for (const re of patterns) {
21083
+ if (re.test(hay)) n++;
21084
+ }
21085
+ if (n > 0) hits[scenario] = n;
21007
21086
  }
21008
- return reversed;
21087
+ return hits;
21009
21088
  }
21010
- function getMapped(row, fieldMap, field) {
21011
- const csvCol = fieldMap[field];
21012
- if (!csvCol) return void 0;
21013
- const val = row[csvCol];
21014
- return val !== void 0 && val !== "" ? val : void 0;
21089
+ function matrixPick(motion, band) {
21090
+ if (motion && band) return MATRIX[motion][band];
21091
+ if (motion) return MOTION_ONLY[motion];
21092
+ if (band) return BAND_ONLY[band];
21093
+ return "even_keel";
21015
21094
  }
21016
- function buildMetadata(row, mappings) {
21017
- const mappedCols = new Set(Object.keys(mappings));
21018
- const metadata = {};
21019
- for (const [key, value] of Object.entries(row)) {
21020
- if (!mappedCols.has(key) && value !== void 0 && value !== "") {
21021
- metadata[key] = value;
21022
- }
21095
+ function reasonFor(scenario, signals, via) {
21096
+ const s = getScenario(scenario);
21097
+ if (via === "keywords") {
21098
+ return `Your notes sound like ${s.label} \u2014 ${s.hook}`;
21023
21099
  }
21024
- return metadata;
21100
+ const motion = signals.salesMotion;
21101
+ const band = signals.dealBand;
21102
+ if (motion && band) {
21103
+ return `${labelMotion(motion)} with ${labelBand(band)} deals maps to ${s.label}.`;
21104
+ }
21105
+ if (motion) return `${labelMotion(motion)} books usually show up as ${s.label}.`;
21106
+ if (band) return `${labelBand(band)} deals usually show up as ${s.label}.`;
21107
+ return `${s.label} is the even-keeled starting book when we don't know the motion yet.`;
21025
21108
  }
21026
- function stripUrl(url) {
21027
- return url.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
21109
+ function labelMotion(m) {
21110
+ return MOTION_FIT_CHOICES.find((c) => c.value === m)?.label ?? m;
21028
21111
  }
21029
- function parseTimestamp(value) {
21030
- if (!value) return null;
21031
- const num2 = Number(value);
21032
- if (!isNaN(num2) && num2 > 1e12) return new Date(num2).toISOString();
21033
- if (!isNaN(num2) && num2 > 1e9) return new Date(num2 * 1e3).toISOString();
21034
- const d = new Date(value);
21035
- if (!isNaN(d.getTime())) return d.toISOString();
21036
- return null;
21112
+ function labelBand(b) {
21113
+ return DEAL_BAND_CHOICES.find((c) => c.value === b)?.label ?? b;
21037
21114
  }
21038
- async function resolveSourceIds(table, sourceSystem, sourceIds) {
21039
- const idMap = /* @__PURE__ */ new Map();
21040
- if (sourceIds.size === 0) return idMap;
21041
- const ids = Array.from(sourceIds);
21042
- const placeholders = ids.map(() => "?").join(", ");
21043
- const rows = await all(
21044
- `SELECT id, source_id FROM ${table} WHERE source_system = ? AND source_id IN (${placeholders})`,
21045
- [sourceSystem, ...ids]
21046
- );
21047
- for (const row of rows) {
21048
- idMap.set(row.source_id, row.id);
21115
+ function inferDemoScenario(signals) {
21116
+ const text = signals.text?.trim() ?? "";
21117
+ if (text) {
21118
+ const hits = keywordHits(text);
21119
+ let best;
21120
+ let bestN = 0;
21121
+ for (const id of NAMED_DEMO_SCENARIOS) {
21122
+ const n = hits[id] ?? 0;
21123
+ if (n > bestN) {
21124
+ best = id;
21125
+ bestN = n;
21126
+ }
21127
+ }
21128
+ if (best && bestN > 0) {
21129
+ return { scenario: best, reason: reasonFor(best, signals, "keywords"), source: "heuristic" };
21130
+ }
21049
21131
  }
21050
- return idMap;
21132
+ const scenario = matrixPick(signals.salesMotion, signals.dealBand);
21133
+ return { scenario, reason: reasonFor(scenario, signals, "matrix"), source: "heuristic" };
21051
21134
  }
21052
- async function importOrganizations(params) {
21053
- const { rows, mappings, sourceSystem } = params;
21054
- const errors = [];
21055
- let imported = 0;
21056
- const fieldMap = reverseMap(mappings);
21057
- for (const row of rows) {
21058
- const canonicalName = getMapped(row, fieldMap, "canonical_name");
21059
- if (!canonicalName) {
21060
- errors.push("Row missing canonical_name");
21061
- continue;
21135
+ function getPreferredDemoScenario() {
21136
+ const raw = getConfigValue(PREF_SCENARIO_KEY);
21137
+ return raw && isNamedDemoScenario(raw) ? raw : void 0;
21138
+ }
21139
+ function saveDemoFit(opts) {
21140
+ setConfigValue(PREF_SCENARIO_KEY, opts.scenario);
21141
+ if (opts.motion) {
21142
+ setConfigValue(PREF_MOTION_KEY, opts.motion);
21143
+ if (opts.syncSalesMotion && !isProfileConfigured(loadProfile())) {
21144
+ setConfigValue("sales-motion", opts.motion);
21062
21145
  }
21063
- const website = getMapped(row, fieldMap, "canonical_domain") ?? "";
21064
- const canonicalDomain = stripUrl(website);
21065
- await insertOrganization({
21066
- canonical_name: canonicalName,
21067
- canonical_domain: canonicalDomain || null,
21068
- source_system: sourceSystem,
21069
- source_id: getMapped(row, fieldMap, "source_id") ?? "",
21070
- raw_data: row,
21071
- metadata: buildMetadata(row, mappings)
21072
- });
21073
- imported++;
21074
21146
  }
21075
- return { imported, errors };
21147
+ if (opts.dealBand) setConfigValue(PREF_BAND_KEY, opts.dealBand);
21076
21148
  }
21077
- async function importPeople(params) {
21078
- const { rows, mappings, sourceSystem } = params;
21079
- const errors = [];
21080
- let imported = 0;
21081
- const fieldMap = reverseMap(mappings);
21082
- const orgSourceIds = /* @__PURE__ */ new Set();
21083
- for (const row of rows) {
21084
- const orgSrcId = getMapped(row, fieldMap, "organization_source_id");
21085
- if (orgSrcId) orgSourceIds.add(orgSrcId);
21086
- }
21087
- const orgIdMap = await resolveSourceIds("organizations", sourceSystem, orgSourceIds);
21088
- for (const row of rows) {
21089
- const firstName = getMapped(row, fieldMap, "first_name") ?? "";
21090
- const lastName = getMapped(row, fieldMap, "last_name") ?? "";
21091
- const canonicalName = [firstName, lastName].filter(Boolean).join(" ") || getMapped(row, fieldMap, "canonical_name") || "";
21092
- if (!canonicalName) {
21093
- errors.push("Row missing name fields");
21094
- continue;
21095
- }
21096
- const orgSourceId = getMapped(row, fieldMap, "organization_source_id");
21097
- await insertPerson({
21098
- canonical_name: canonicalName,
21099
- canonical_email: getMapped(row, fieldMap, "canonical_email") || null,
21100
- organization_id: orgSourceId ? orgIdMap.get(orgSourceId) ?? null : null,
21101
- source_system: sourceSystem,
21102
- source_id: getMapped(row, fieldMap, "source_id") ?? "",
21103
- raw_data: row,
21104
- metadata: buildMetadata(row, mappings)
21105
- });
21106
- imported++;
21107
- }
21108
- return { imported, errors };
21149
+ function scenarioMenuChoices() {
21150
+ return NAMED_DEMO_SCENARIOS.map((id) => {
21151
+ const s = getScenario(id);
21152
+ return {
21153
+ value: id,
21154
+ label: s.label,
21155
+ description: s.hook
21156
+ };
21157
+ });
21109
21158
  }
21110
- async function importOpportunities(params) {
21111
- const { rows, mappings, sourceSystem } = params;
21112
- const errors = [];
21113
- let imported = 0;
21114
- const fieldMap = reverseMap(mappings);
21115
- const orgSourceIds = /* @__PURE__ */ new Set();
21116
- const ownerSourceIds = /* @__PURE__ */ new Set();
21117
- for (const row of rows) {
21118
- const o = getMapped(row, fieldMap, "organization_source_id");
21119
- if (o) orgSourceIds.add(o);
21120
- const w = getMapped(row, fieldMap, "owner_source_id");
21121
- if (w) ownerSourceIds.add(w);
21122
- }
21123
- const orgIdMap = await resolveSourceIds("organizations", sourceSystem, orgSourceIds);
21124
- const ownerIdMap = await resolveSourceIds("people", sourceSystem, ownerSourceIds);
21125
- for (const row of rows) {
21126
- const canonicalName = getMapped(row, fieldMap, "canonical_name");
21127
- if (!canonicalName) {
21128
- errors.push("Row missing canonical_name");
21129
- continue;
21130
- }
21131
- const orgSourceId = getMapped(row, fieldMap, "organization_source_id");
21132
- const ownerSourceId = getMapped(row, fieldMap, "owner_source_id");
21133
- const amountStr = getMapped(row, fieldMap, "amount");
21134
- await insertOpportunity({
21135
- canonical_name: canonicalName,
21136
- organization_id: orgSourceId ? orgIdMap.get(orgSourceId) ?? null : null,
21137
- owner_id: ownerSourceId ? ownerIdMap.get(ownerSourceId) ?? null : null,
21138
- current_stage: getMapped(row, fieldMap, "current_stage") || null,
21139
- amount: amountStr ? parseFloat(amountStr) || null : null,
21140
- close_date: getMapped(row, fieldMap, "close_date") || null,
21141
- source_system: sourceSystem,
21142
- source_id: getMapped(row, fieldMap, "source_id") ?? "",
21143
- raw_data: row,
21144
- metadata: buildMetadata(row, mappings)
21145
- });
21146
- imported++;
21159
+ var PREF_SCENARIO_KEY, PREF_MOTION_KEY, PREF_BAND_KEY, MOTION_FIT_CHOICES, DEAL_BAND_CHOICES, MATRIX, MOTION_ONLY, BAND_ONLY, KEYWORD_HINTS;
21160
+ var init_scenario_fit = __esm({
21161
+ "src/demo/scenario-fit.ts"() {
21162
+ "use strict";
21163
+ init_store();
21164
+ init_profile();
21165
+ init_scenarios();
21166
+ PREF_SCENARIO_KEY = "demo-scenario-preference";
21167
+ PREF_MOTION_KEY = "demo-fit-motion";
21168
+ PREF_BAND_KEY = "demo-fit-deal-band";
21169
+ MOTION_FIT_CHOICES = [
21170
+ {
21171
+ value: "plg",
21172
+ label: "Product-led / self-serve",
21173
+ description: "Users start themselves; sales assists or expands"
21174
+ },
21175
+ {
21176
+ value: "smb_velocity",
21177
+ label: "High-volume SMB",
21178
+ description: "Fast cycles, lots of small deals, outbound or inbound machine"
21179
+ },
21180
+ {
21181
+ value: "mid_market",
21182
+ label: "Mid-market, structured process",
21183
+ description: "A real sales cycle, a few stakeholders, moderate ACV"
21184
+ },
21185
+ {
21186
+ value: "enterprise",
21187
+ label: "Enterprise, long cycles",
21188
+ description: "Large deals, many buyers, quarters not weeks"
21189
+ }
21190
+ ];
21191
+ DEAL_BAND_CHOICES = [
21192
+ {
21193
+ value: "velocity",
21194
+ label: "Under ~$15K, days to a couple of weeks",
21195
+ description: "Velocity / transactional"
21196
+ },
21197
+ {
21198
+ value: "core",
21199
+ label: "~$15K\u2013$50K, a few weeks",
21200
+ description: "Core SMB"
21201
+ },
21202
+ {
21203
+ value: "mid",
21204
+ label: "~$50K\u2013$150K, 1\u20133 months",
21205
+ description: "Classic mid-market"
21206
+ },
21207
+ {
21208
+ value: "enterprise",
21209
+ label: "$150K+, a quarter or more",
21210
+ description: "Enterprise / strategic"
21211
+ }
21212
+ ];
21213
+ MATRIX = {
21214
+ plg: {
21215
+ velocity: "leaky_bucket",
21216
+ core: "leaky_bucket",
21217
+ mid: "hidden_crisis",
21218
+ enterprise: "hidden_crisis"
21219
+ },
21220
+ smb_velocity: {
21221
+ velocity: "busy_bees",
21222
+ core: "busy_bees",
21223
+ mid: "leaky_bucket",
21224
+ enterprise: "lone_wolf"
21225
+ },
21226
+ mid_market: {
21227
+ velocity: "busy_bees",
21228
+ core: "stale_pipeline",
21229
+ mid: "stale_pipeline",
21230
+ enterprise: "hidden_crisis"
21231
+ },
21232
+ enterprise: {
21233
+ velocity: "lone_wolf",
21234
+ core: "lone_wolf",
21235
+ mid: "hidden_crisis",
21236
+ enterprise: "hidden_crisis"
21237
+ }
21238
+ };
21239
+ MOTION_ONLY = {
21240
+ plg: "leaky_bucket",
21241
+ smb_velocity: "busy_bees",
21242
+ mid_market: "stale_pipeline",
21243
+ enterprise: "hidden_crisis"
21244
+ };
21245
+ BAND_ONLY = {
21246
+ velocity: "busy_bees",
21247
+ core: "leaky_bucket",
21248
+ mid: "stale_pipeline",
21249
+ enterprise: "hidden_crisis"
21250
+ };
21251
+ KEYWORD_HINTS = [
21252
+ { scenario: "even_keel", patterns: [/\beven keel\b/, /\breasonably healthy\b/, /\bno crisis\b/, /\bjust evaluating\b/, /\bgreen[- ]field\b/] },
21253
+ { scenario: "compound_pain", patterns: [/\beverything('?s| is) (on fire|red|broken)\b/, /\bcompound\b/, /\ball (five )?vitals\b/, /\bmultiple problems\b/] },
21254
+ { scenario: "leaky_bucket", patterns: [/\bhandoff\b/, /\bmqls?\b/, /\bleak/, /\bdrop[- ]rate/, /\bvanish/, /\brouting\b/, /\bmarketing.?sales\b/] },
21255
+ { scenario: "stale_pipeline", patterns: [/\bzombie/, /\bstale\b/, /\bpast[- ]due\b/, /\bforecast(ing)? on fiction\b/, /\bstuck in negotiation\b/, /\bquiet deals?\b/] },
21256
+ { scenario: "lone_wolf", patterns: [/\bsingle[- ]thread/, /\blone wolf\b/, /\bone contact\b/, /\bchampion leaves\b/] },
21257
+ { scenario: "busy_bees", patterns: [/\bspray/, /\bnois(e|y)\b/, /\bmisdirected\b/, /\bactivity (volume|metrics)\b/, /\bbusy bees\b/, /\bnot (on|hitting) pipeline\b/] },
21258
+ { scenario: "hidden_crisis", patterns: [/\bhidden crisis\b/, /\benterprise (is )?(dying|red|stale)\b/, /\bsegment.{0,20}mask/, /\baverages? (look|looks) (fine|okay|yellow)\b/] }
21259
+ ];
21147
21260
  }
21148
- return { imported, errors };
21261
+ });
21262
+
21263
+ // src/conversation/onboard-tiers.ts
21264
+ import { existsSync as existsSync22, statSync as statSync4 } from "fs";
21265
+ function flagSet(tier) {
21266
+ return Boolean(getConfigValue(TIER_CONFIG_KEYS[tier]));
21149
21267
  }
21150
- async function importActivities(params) {
21151
- const { rows, mappings, sourceSystem } = params;
21152
- const errors = [];
21153
- let imported = 0;
21154
- const fieldMap = reverseMap(mappings);
21155
- const personSourceIds = /* @__PURE__ */ new Set();
21156
- for (const row of rows) {
21157
- const p = getMapped(row, fieldMap, "person_source_id");
21158
- if (p) personSourceIds.add(p);
21159
- }
21160
- const personIdMap = await resolveSourceIds("people", sourceSystem, personSourceIds);
21161
- for (const row of rows) {
21162
- const rawType = getMapped(row, fieldMap, "activity_type") ?? "custom";
21163
- const activityType = ACTIVITY_TYPE_MAP[rawType] ?? "custom";
21164
- const rawOccurredAt = getMapped(row, fieldMap, "occurred_at") ?? "";
21165
- const occurredAt = parseTimestamp(rawOccurredAt);
21166
- if (!occurredAt) {
21167
- errors.push("Row missing or invalid occurred_at");
21168
- continue;
21169
- }
21170
- const personSourceId = getMapped(row, fieldMap, "person_source_id");
21171
- await insertActivity({
21172
- activity_type: activityType,
21173
- occurred_at: occurredAt,
21174
- person_id: personSourceId ? personIdMap.get(personSourceId) ?? null : null,
21175
- organization_id: null,
21176
- opportunity_id: null,
21177
- source_system: sourceSystem,
21178
- source_id: getMapped(row, fieldMap, "source_id") ?? "",
21179
- raw_data: row,
21180
- metadata: buildMetadata(row, mappings)
21181
- });
21182
- imported++;
21268
+ function markOnboardTierComplete(...tiers) {
21269
+ const at = (/* @__PURE__ */ new Date()).toISOString();
21270
+ for (const tier of tiers) {
21271
+ if (!flagSet(tier)) setConfigValue(TIER_CONFIG_KEYS[tier], at);
21183
21272
  }
21184
- return { imported, errors };
21185
21273
  }
21186
- async function importCampaigns(params) {
21187
- const { rows, mappings, sourceSystem } = params;
21188
- const errors = [];
21189
- let imported = 0;
21190
- const fieldMap = reverseMap(mappings);
21191
- for (const row of rows) {
21192
- const canonicalName = getMapped(row, fieldMap, "canonical_name");
21193
- if (!canonicalName) {
21194
- errors.push("Row missing canonical_name");
21195
- continue;
21196
- }
21197
- const rawType = row["sequence_type"] ?? "";
21198
- const campaignType = CAMPAIGN_TYPE_MAP[rawType] ?? "email_sequence";
21199
- await insertCampaign({
21200
- canonical_name: canonicalName,
21201
- campaign_type: campaignType,
21202
- source_system: sourceSystem,
21203
- source_id: getMapped(row, fieldMap, "source_id") ?? "",
21204
- raw_data: row,
21205
- metadata: buildMetadata(row, mappings)
21206
- });
21207
- imported++;
21208
- }
21209
- return { imported, errors };
21274
+ function markProductionDataSeen() {
21275
+ markOnboardTierComplete("production");
21210
21276
  }
21211
- var init_importer = __esm({
21212
- "src/pipeline/importer.ts"() {
21277
+ function markDemoDataSeen() {
21278
+ markOnboardTierComplete("demo");
21279
+ }
21280
+ var TIER_CONFIG_KEYS;
21281
+ var init_onboard_tiers = __esm({
21282
+ "src/conversation/onboard-tiers.ts"() {
21213
21283
  "use strict";
21214
- init_csv_mappings();
21215
- init_queries();
21216
- init_connection();
21284
+ init_store();
21285
+ init_profile();
21286
+ init_repl_api();
21287
+ init_scenario_fit();
21288
+ TIER_CONFIG_KEYS = {
21289
+ profile: "onboard-tier-profile",
21290
+ domain: "onboard-tier-domain",
21291
+ demo: "onboard-tier-demo",
21292
+ production: "onboard-tier-production"
21293
+ };
21217
21294
  }
21218
21295
  });
21219
21296
 
21220
- // src/pipeline/resolver.ts
21221
- async function resolveIdentities() {
21222
- const orgsResolved = await resolveOrganizations();
21223
- const peopleResolved = await resolvePeople();
21224
- return { resolved: orgsResolved + peopleResolved, peopleResolved, orgsResolved };
21297
+ // src/pipeline/csv-parse.ts
21298
+ var csv_parse_exports = {};
21299
+ __export(csv_parse_exports, {
21300
+ getPreviewRows: () => getPreviewRows,
21301
+ parseCSV: () => parseCSV
21302
+ });
21303
+ import Papa from "papaparse";
21304
+ function parseCSV(content) {
21305
+ const result = Papa.parse(content, {
21306
+ header: true,
21307
+ skipEmptyLines: true,
21308
+ transformHeader: (h) => h.trim()
21309
+ });
21310
+ return {
21311
+ headers: result.meta.fields ?? [],
21312
+ rows: result.data,
21313
+ rowCount: result.data.length
21314
+ };
21225
21315
  }
21226
- async function resolveOrganizations() {
21227
- let resolved = 0;
21228
- const orgs = await all(
21229
- `SELECT id, canonical_domain, created_at FROM organizations WHERE canonical_id IS NULL AND canonical_domain IS NOT NULL ORDER BY created_at ASC`
21230
- );
21231
- const groups = /* @__PURE__ */ new Map();
21232
- for (const org of orgs) {
21233
- const key = org.canonical_domain.toLowerCase();
21234
- const group = groups.get(key);
21235
- if (group) group.push(org);
21236
- else groups.set(key, [org]);
21237
- }
21238
- for (const [, group] of groups) {
21239
- if (group.length <= 1) continue;
21240
- const canonical = group[0];
21241
- const duplicateIds = group.slice(1).map((d) => d.id);
21242
- const placeholders = duplicateIds.map(() => "?").join(", ");
21243
- await run(`UPDATE organizations SET canonical_id = ? WHERE id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21244
- await run(`UPDATE people SET organization_id = ? WHERE organization_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21245
- await run(`UPDATE opportunities SET organization_id = ? WHERE organization_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21246
- await run(`UPDATE activities SET organization_id = ? WHERE organization_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21247
- resolved += duplicateIds.length;
21248
- }
21249
- return resolved;
21250
- }
21251
- async function resolvePeople() {
21252
- let resolved = 0;
21253
- const people = await all(
21254
- `SELECT id, canonical_email, organization_id, created_at FROM people WHERE canonical_id IS NULL AND canonical_email IS NOT NULL ORDER BY created_at ASC`
21255
- );
21256
- const groups = /* @__PURE__ */ new Map();
21257
- for (const person of people) {
21258
- const key = person.canonical_email.toLowerCase();
21259
- const group = groups.get(key);
21260
- if (group) group.push(person);
21261
- else groups.set(key, [person]);
21262
- }
21263
- for (const [, group] of groups) {
21264
- if (group.length <= 1) continue;
21265
- const canonical = group[0];
21266
- const duplicates = group.slice(1);
21267
- const duplicateIds = duplicates.map((d) => d.id);
21268
- const placeholders = duplicateIds.map(() => "?").join(", ");
21269
- if (!canonical.organization_id) {
21270
- const withOrg = duplicates.find((d) => d.organization_id);
21271
- if (withOrg) {
21272
- await run(`UPDATE people SET organization_id = ? WHERE id = ?`, [withOrg.organization_id, canonical.id]);
21273
- }
21274
- }
21275
- await run(`UPDATE people SET canonical_id = ? WHERE id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21276
- await run(`UPDATE activities SET person_id = ? WHERE person_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21277
- await run(`UPDATE opportunities SET owner_id = ? WHERE owner_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21278
- resolved += duplicateIds.length;
21279
- }
21280
- return resolved;
21316
+ function getPreviewRows(rows, count = 5) {
21317
+ return rows.slice(0, count);
21281
21318
  }
21282
- var init_resolver2 = __esm({
21283
- "src/pipeline/resolver.ts"() {
21319
+ var init_csv_parse = __esm({
21320
+ "src/pipeline/csv-parse.ts"() {
21284
21321
  "use strict";
21285
- init_connection();
21286
21322
  }
21287
21323
  });
21288
21324
 
21289
- // src/demo/seed.ts
21290
- function mulberry32(seed) {
21291
- let a = seed | 0;
21292
- return () => {
21293
- a = a + 1831565813 | 0;
21294
- let t = Math.imul(a ^ a >>> 15, 1 | a);
21295
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
21296
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
21297
- };
21298
- }
21299
- function createSeededRandom(seed) {
21300
- const raw = mulberry32(seed);
21301
- const rng = {
21302
- next: raw,
21303
- nextInt(min, max) {
21304
- return Math.floor(raw() * (max - min + 1)) + min;
21305
- },
21306
- nextFloat(min, max) {
21307
- return raw() * (max - min) + min;
21308
- },
21309
- pick(arr) {
21310
- if (arr.length === 0) {
21311
- throw new Error("Cannot pick from an empty array");
21312
- }
21313
- return arr[Math.floor(raw() * arr.length)];
21314
- },
21315
- pickN(arr, n) {
21316
- const copy = [...arr];
21317
- rng.shuffle(copy);
21318
- return copy.slice(0, Math.min(n, copy.length));
21319
- },
21320
- shuffle(arr) {
21321
- for (let i = arr.length - 1; i > 0; i--) {
21322
- const j = Math.floor(raw() * (i + 1));
21323
- const current = arr[i];
21324
- arr[i] = arr[j];
21325
- arr[j] = current;
21326
- }
21327
- return arr;
21328
- },
21329
- chance(probability) {
21330
- return raw() < probability;
21331
- },
21332
- uuid() {
21333
- const bytes = Array.from({ length: 16 }, () => Math.floor(raw() * 256));
21334
- bytes[6] = bytes[6] & 15 | 64;
21335
- bytes[8] = bytes[8] & 63 | 128;
21336
- const hex = bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
21337
- return [
21338
- hex.slice(0, 8),
21339
- hex.slice(8, 12),
21340
- hex.slice(12, 16),
21341
- hex.slice(16, 20),
21342
- hex.slice(20, 32)
21343
- ].join("-");
21344
- },
21345
- date(start, end) {
21346
- const s = start.getTime();
21347
- const e = end.getTime();
21348
- return new Date(s + raw() * (e - s));
21349
- },
21350
- weightedPick(items, weights) {
21351
- if (items.length === 0) {
21352
- throw new Error("Cannot pick from an empty weighted item list");
21353
- }
21354
- const total = weights.reduce((sum, w) => sum + w, 0);
21355
- let r = raw() * total;
21356
- for (let i = 0; i < items.length; i++) {
21357
- r -= weights[i] ?? 0;
21358
- if (r <= 0) return items[i];
21359
- }
21360
- return items[items.length - 1];
21361
- }
21362
- };
21363
- return rng;
21325
+ // src/pipeline/csv-mappings.ts
21326
+ function getDefaultMappings(sourceSystem, entityType) {
21327
+ return DEFAULT_MAPPINGS[sourceSystem]?.[entityType] ?? {};
21364
21328
  }
21365
- var init_seed = __esm({
21366
- "src/demo/seed.ts"() {
21329
+ var DEFAULT_MAPPINGS, ACTIVITY_TYPE_MAP, CAMPAIGN_TYPE_MAP;
21330
+ var init_csv_mappings = __esm({
21331
+ "src/pipeline/csv-mappings.ts"() {
21367
21332
  "use strict";
21333
+ DEFAULT_MAPPINGS = {
21334
+ salesforce: {
21335
+ organizations: { Name: "canonical_name", Website: "canonical_domain", Id: "source_id" },
21336
+ people: { Email: "canonical_email", FirstName: "first_name", LastName: "last_name", Id: "source_id", AccountId: "organization_source_id" },
21337
+ opportunities: { Name: "canonical_name", Amount: "amount", StageName: "current_stage", CloseDate: "close_date", Id: "source_id", AccountId: "organization_source_id", OwnerId: "owner_source_id" },
21338
+ activities: { TaskSubtype: "activity_type", ActivityDate: "occurred_at", Id: "source_id", WhoId: "person_source_id" }
21339
+ },
21340
+ hubspot: {
21341
+ organizations: { "Company name": "canonical_name", "Company Domain Name": "canonical_domain", "Company ID": "source_id" },
21342
+ people: { Email: "canonical_email", "First Name": "first_name", "Last Name": "last_name", "Contact ID": "source_id", "Associated Company ID": "organization_source_id" },
21343
+ activities: { Type: "activity_type", "Created At": "occurred_at", "Engagement ID": "source_id", "Contact ID": "person_source_id" }
21344
+ },
21345
+ outreach: {
21346
+ campaigns: { name: "canonical_name", id: "source_id" },
21347
+ activities: { type: "activity_type", created_at: "occurred_at", id: "source_id", prospect_id: "person_source_id" }
21348
+ }
21349
+ };
21350
+ ACTIVITY_TYPE_MAP = {
21351
+ Call: "call",
21352
+ Email: "email",
21353
+ Task: "custom",
21354
+ CALL: "call",
21355
+ EMAIL: "email",
21356
+ MEETING: "meeting",
21357
+ NOTE: "custom",
21358
+ TASK: "custom",
21359
+ INCOMING_EMAIL: "email",
21360
+ FORWARDED_EMAIL: "email",
21361
+ call: "call",
21362
+ email: "email",
21363
+ sequence_step: "email"
21364
+ };
21365
+ CAMPAIGN_TYPE_MAP = {
21366
+ outbound: "email_sequence",
21367
+ inbound: "email_sequence",
21368
+ trigger: "email_sequence"
21369
+ };
21368
21370
  }
21369
21371
  });
21370
21372
 
21371
- // src/demo/scenarios.ts
21372
- var scenarios_exports = {};
21373
- __export(scenarios_exports, {
21374
- NAMED_DEMO_SCENARIOS: () => NAMED_DEMO_SCENARIOS,
21375
- SCENARIOS: () => SCENARIOS,
21376
- SCENARIO_LIST: () => SCENARIO_LIST,
21377
- blendScenarios: () => blendScenarios,
21378
- getScenario: () => getScenario,
21379
- isNamedDemoScenario: () => isNamedDemoScenario,
21380
- pickRandomScenario: () => pickRandomScenario,
21381
- resolveScenarioInput: () => resolveScenarioInput
21373
+ // src/pipeline/csv-detect.ts
21374
+ var csv_detect_exports = {};
21375
+ __export(csv_detect_exports, {
21376
+ detectEntityType: () => detectEntityType
21382
21377
  });
21383
- function getScenario(key) {
21384
- const scenario = SCENARIOS[key];
21385
- if (!scenario) {
21386
- throw new Error(`Unknown scenario: ${key}. Valid: ${Object.keys(SCENARIOS).join(", ")}`);
21387
- }
21388
- return scenario;
21389
- }
21390
- function isNamedDemoScenario(raw) {
21391
- return NAMED_DEMO_SCENARIOS.includes(raw);
21392
- }
21393
- function resolveScenarioInput(raw) {
21394
- const input = raw?.trim();
21395
- if (!input) return void 0;
21396
- if (input === "research_blend") return "research_blend";
21397
- if (NAMED_DEMO_SCENARIOS.includes(input)) {
21398
- return input;
21378
+ function detectEntityType(headers, sourceSystem) {
21379
+ const rules = SIGNATURES[sourceSystem];
21380
+ if (!rules) return null;
21381
+ const headerSet = new Set(headers);
21382
+ let bestMatch = null;
21383
+ for (const rule of rules) {
21384
+ const requiredMatches = rule.required.filter((col) => headerSet.has(col)).length;
21385
+ const requiredTotal = rule.required.length;
21386
+ if (requiredMatches < Math.ceil(requiredTotal / 2)) continue;
21387
+ const optionalMatches = rule.optional.filter((col) => headerSet.has(col)).length;
21388
+ const optionalTotal = rule.optional.length;
21389
+ const score = requiredMatches / requiredTotal * 0.7 + (optionalTotal > 0 ? optionalMatches / optionalTotal * 0.3 : 0.3);
21390
+ if (!bestMatch || score > bestMatch.score) {
21391
+ bestMatch = { rule, score };
21392
+ }
21399
21393
  }
21400
- const n = Number(input);
21401
- if (Number.isInteger(n) && n >= 1 && n <= NAMED_DEMO_SCENARIOS.length) {
21402
- return NAMED_DEMO_SCENARIOS[n - 1];
21394
+ if (!bestMatch) return null;
21395
+ const defaultMappings = getDefaultMappings(sourceSystem, bestMatch.rule.entityType);
21396
+ const mappings = {};
21397
+ for (const [csvCol, ontologyField] of Object.entries(defaultMappings)) {
21398
+ if (headerSet.has(csvCol)) {
21399
+ mappings[csvCol] = ontologyField;
21400
+ }
21403
21401
  }
21404
- return null;
21405
- }
21406
- function pickRandomScenario() {
21407
- return RANDOM_POOL[Math.floor(Math.random() * RANDOM_POOL.length)];
21408
- }
21409
- function blendScenarios(_research) {
21410
- const blended = {
21411
- ...BASELINE,
21412
- key: "research_blend",
21413
- label: "Research-Derived Blend",
21414
- description: "Realistic data with mild-to-moderate problems across all vital signs.",
21415
- story: "Generated with default research blend. Problems are seeded across all vital signs at realistic levels.",
21416
- hook: "Mild-to-moderate problems seeded across all five vitals.",
21417
- // Bump all problems slightly above baseline for discoverability
21418
- staleContactRatio: 0.2,
21419
- pastCloseDateRatio: 0.15,
21420
- mqlDropRatio: 0.15,
21421
- qualifiedNoOutreachRatio: 0.15,
21422
- stuckDealRatio: 0.15,
21423
- noiseActivityRatio: 0.2,
21424
- singleThreadRatio: 0.25
21402
+ return {
21403
+ entityType: bestMatch.rule.entityType,
21404
+ confidence: Math.round(bestMatch.score * 100) / 100,
21405
+ mappings
21425
21406
  };
21426
- return blended;
21427
21407
  }
21428
- var BASELINE, SCENARIOS, SCENARIO_LIST, NAMED_DEMO_SCENARIOS, RANDOM_POOL;
21429
- var init_scenarios = __esm({
21430
- "src/demo/scenarios.ts"() {
21408
+ var SIGNATURES;
21409
+ var init_csv_detect = __esm({
21410
+ "src/pipeline/csv-detect.ts"() {
21431
21411
  "use strict";
21432
- BASELINE = {
21433
- enterpriseRatio: 0.2,
21434
- midMarketRatio: 0.3,
21435
- smbRatio: 0.5,
21436
- staleContactRatio: 0.15,
21437
- staleContactRatioEnterprise: 0.2,
21438
- staleContactRatioSmb: 0.1,
21439
- pastCloseDateRatio: 0.1,
21440
- staleDays: 120,
21441
- mqlDropRatio: 0.1,
21442
- qualifiedNoOutreachRatio: 0.1,
21443
- stuckDealRatio: 0.1,
21444
- stuckInNegotiationDays: 45,
21445
- avgDaysPerStageEnterprise: 25,
21446
- avgDaysPerStageSmb: 8,
21447
- activityVolumeMultiplier: 1,
21448
- noiseActivityRatio: 0.15,
21449
- singleThreadRatio: 0.2,
21450
- loneWolfRepIndex: null,
21451
- loneWolfSingleThreadRatio: 0,
21452
- freshnessGapDays: 90
21412
+ init_csv_mappings();
21413
+ SIGNATURES = {
21414
+ salesforce: [
21415
+ { entityType: "organizations", required: ["Name", "Industry"], optional: ["AnnualRevenue", "BillingCountry", "Website", "NumberOfEmployees"], weight: 1 },
21416
+ { entityType: "people", required: ["FirstName", "LastName", "Email"], optional: ["AccountId", "Title", "Phone"], weight: 1 },
21417
+ { entityType: "opportunities", required: ["StageName", "Amount", "CloseDate"], optional: ["AccountId", "OwnerId", "Probability"], weight: 1 },
21418
+ { entityType: "activities", required: ["Subject", "ActivityDate", "TaskSubtype"], optional: ["WhoId", "WhatId", "Status"], weight: 1 }
21419
+ ],
21420
+ hubspot: [
21421
+ { entityType: "organizations", required: ["Company name", "Company Domain Name"], optional: ["Company ID", "Industry", "Annual Revenue"], weight: 1 },
21422
+ { entityType: "people", required: ["First Name", "Last Name", "Email"], optional: ["Contact ID", "Associated Company ID", "Lifecycle Stage"], weight: 1 },
21423
+ { entityType: "activities", required: ["Engagement ID", "Type"], optional: ["Timestamp", "Created At", "Contact ID"], weight: 1 }
21424
+ ],
21425
+ outreach: [
21426
+ { entityType: "campaigns", required: ["sequence_type", "step_count"], optional: ["name", "id", "enabled"], weight: 1 },
21427
+ { entityType: "activities", required: ["prospect_id", "prospect_email"], optional: ["type", "created_at", "subject"], weight: 1 }
21428
+ ]
21453
21429
  };
21454
- SCENARIOS = {
21455
- hidden_crisis: {
21456
- ...BASELINE,
21457
- key: "hidden_crisis",
21458
- label: "The Hidden Crisis",
21459
- description: "Overall health looks yellow but Enterprise is deep red, masked by strong SMB numbers.",
21460
- story: "Your aggregate numbers look okay \u2014 but when you break it by segment, Enterprise is dying. 60% of enterprise contacts have gone dark, deals are single-threaded, and SMB is carrying the average.",
21461
- hook: "SMB is carrying the average while Enterprise dies quietly.",
21462
- staleContactRatio: 0.3,
21463
- staleContactRatioEnterprise: 0.6,
21464
- staleContactRatioSmb: 0.1,
21465
- singleThreadRatio: 0.5,
21466
- enterpriseRatio: 0.3,
21467
- midMarketRatio: 0.3,
21468
- smbRatio: 0.4
21469
- },
21470
- leaky_bucket: {
21471
- ...BASELINE,
21472
- key: "leaky_bucket",
21473
- label: "The Leaky Bucket",
21474
- description: "Marketing generates plenty of leads but 40% vanish at handoff to sales.",
21475
- story: "Marketing is doing its job \u2014 MQLs are flowing. But 40% of qualified leads never show up in sales workflows. They're falling through the cracks at handoff, and nobody's noticing because marketing reports MQL count and sales reports pipeline value.",
21476
- hook: "MQLs flow in, then 40% vanish at the sales handoff.",
21477
- mqlDropRatio: 0.4,
21478
- qualifiedNoOutreachRatio: 0.35,
21479
- staleContactRatio: 0.2
21480
- },
21481
- stale_pipeline: {
21482
- ...BASELINE,
21483
- key: "stale_pipeline",
21484
- label: "The Stale Pipeline",
21485
- description: "Big pipeline number but half the deals are zombies stuck in late stages.",
21486
- story: "The pipeline report says $5M. But look closer: half those deals have close dates in the past, 40% are stuck in Negotiation for 120+ days, and nobody's touching them. You're forecasting on fiction.",
21487
- hook: "Half the pipeline is zombies \u2014 you're forecasting on fiction.",
21488
- pastCloseDateRatio: 0.5,
21489
- stuckDealRatio: 0.4,
21490
- stuckInNegotiationDays: 120,
21491
- staleContactRatio: 0.25,
21492
- staleDays: 90
21493
- },
21494
- lone_wolf: {
21495
- ...BASELINE,
21496
- key: "lone_wolf",
21497
- label: "The Lone Wolf",
21498
- description: "One rep has great numbers but every single deal is single-threaded.",
21499
- story: "Your top rep is crushing it on paper \u2014 biggest pipeline, highest close rate. But every deal has exactly one contact. One champion goes on vacation, gets promoted, or leaves, and the entire pipeline collapses.",
21500
- hook: "Top rep, huge pipeline, one contact per deal \u2014 one exit from collapse.",
21501
- loneWolfRepIndex: 0,
21502
- loneWolfSingleThreadRatio: 1,
21503
- singleThreadRatio: 0.15
21504
- },
21505
- busy_bees: {
21506
- ...BASELINE,
21507
- key: "busy_bees",
21508
- label: "The Busy Bees",
21509
- description: "High activity volume across the team, but most of it hits dead ends.",
21510
- story: "Your team is busy. Activity metrics look great \u2014 calls are up, emails are up, meetings are up. But 60% of that activity is aimed at contacts with no associated pipeline. Reps are spraying, not aiming.",
21511
- hook: "Reps are spraying, not aiming.",
21512
- activityVolumeMultiplier: 3,
21513
- noiseActivityRatio: 0.6,
21514
- staleContactRatio: 0.2
21515
- },
21516
- even_keel: {
21517
- ...BASELINE,
21518
- key: "even_keel",
21519
- label: "The Even Keel",
21520
- description: "A reasonably healthy book \u2014 enough yellow to listen, not a five-alarm fire.",
21521
- story: "Most numbers sit in a normal band. A few contacts have gone quiet, a handful of deals are slow, activity is mostly on-pipeline. This is what 'fine' looks like on the stethoscope \u2014 useful when you want to evaluate NTRP without a manufactured crisis.",
21522
- hook: "Reasonably healthy \u2014 enough signal to listen, not a crisis."
21523
- },
21524
- compound_pain: {
21525
- ...BASELINE,
21526
- key: "compound_pain",
21527
- label: "The Compound Fracture",
21528
- description: "Several vitals are red at once \u2014 stale pipeline, leaky handoff, noisy activity, thin threads.",
21529
- story: "This isn't one problem. Enterprise contacts have gone dark, MQLs vanish at handoff, late-stage deals are zombies, and a lot of activity never touches pipeline. The gating logic has to pick a first red \u2014 that's the point of this book.",
21530
- hook: "Several vitals red at once \u2014 the stethoscope has to pick a first listen.",
21531
- enterpriseRatio: 0.3,
21532
- midMarketRatio: 0.3,
21533
- smbRatio: 0.4,
21534
- staleContactRatio: 0.35,
21535
- staleContactRatioEnterprise: 0.55,
21536
- staleContactRatioSmb: 0.15,
21537
- pastCloseDateRatio: 0.35,
21538
- staleDays: 100,
21539
- mqlDropRatio: 0.3,
21540
- qualifiedNoOutreachRatio: 0.25,
21541
- stuckDealRatio: 0.3,
21542
- stuckInNegotiationDays: 90,
21543
- activityVolumeMultiplier: 2,
21544
- noiseActivityRatio: 0.4,
21545
- singleThreadRatio: 0.4
21430
+ }
21431
+ });
21432
+
21433
+ // src/pipeline/importer.ts
21434
+ async function importRows(params) {
21435
+ let result;
21436
+ switch (params.entityType) {
21437
+ case "organizations":
21438
+ result = await importOrganizations(params);
21439
+ break;
21440
+ case "people":
21441
+ result = await importPeople(params);
21442
+ break;
21443
+ case "opportunities":
21444
+ result = await importOpportunities(params);
21445
+ break;
21446
+ case "activities":
21447
+ result = await importActivities(params);
21448
+ break;
21449
+ case "campaigns":
21450
+ result = await importCampaigns(params);
21451
+ break;
21452
+ default:
21453
+ result = { imported: 0, errors: [`Unknown entity type: ${params.entityType}`] };
21454
+ }
21455
+ const { invalidateLexiconSeed: invalidateLexiconSeed2 } = await Promise.resolve().then(() => (init_lexicon_seed(), lexicon_seed_exports));
21456
+ invalidateLexiconSeed2();
21457
+ return result;
21458
+ }
21459
+ function reverseMap(mappings) {
21460
+ const reversed = {};
21461
+ for (const [csvCol, ontologyField] of Object.entries(mappings)) {
21462
+ reversed[ontologyField] = csvCol;
21463
+ }
21464
+ return reversed;
21465
+ }
21466
+ function getMapped(row, fieldMap, field) {
21467
+ const csvCol = fieldMap[field];
21468
+ if (!csvCol) return void 0;
21469
+ const val = row[csvCol];
21470
+ return val !== void 0 && val !== "" ? val : void 0;
21471
+ }
21472
+ function buildMetadata(row, mappings) {
21473
+ const mappedCols = new Set(Object.keys(mappings));
21474
+ const metadata = {};
21475
+ for (const [key, value] of Object.entries(row)) {
21476
+ if (!mappedCols.has(key) && value !== void 0 && value !== "") {
21477
+ metadata[key] = value;
21478
+ }
21479
+ }
21480
+ return metadata;
21481
+ }
21482
+ function stripUrl(url) {
21483
+ return url.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
21484
+ }
21485
+ function parseTimestamp(value) {
21486
+ if (!value) return null;
21487
+ const num2 = Number(value);
21488
+ if (!isNaN(num2) && num2 > 1e12) return new Date(num2).toISOString();
21489
+ if (!isNaN(num2) && num2 > 1e9) return new Date(num2 * 1e3).toISOString();
21490
+ const d = new Date(value);
21491
+ if (!isNaN(d.getTime())) return d.toISOString();
21492
+ return null;
21493
+ }
21494
+ async function resolveSourceIds(table, sourceSystem, sourceIds) {
21495
+ const idMap = /* @__PURE__ */ new Map();
21496
+ if (sourceIds.size === 0) return idMap;
21497
+ const ids = Array.from(sourceIds);
21498
+ const placeholders = ids.map(() => "?").join(", ");
21499
+ const rows = await all(
21500
+ `SELECT id, source_id FROM ${table} WHERE source_system = ? AND source_id IN (${placeholders})`,
21501
+ [sourceSystem, ...ids]
21502
+ );
21503
+ for (const row of rows) {
21504
+ idMap.set(row.source_id, row.id);
21505
+ }
21506
+ return idMap;
21507
+ }
21508
+ async function importOrganizations(params) {
21509
+ const { rows, mappings, sourceSystem } = params;
21510
+ const errors = [];
21511
+ let imported = 0;
21512
+ const fieldMap = reverseMap(mappings);
21513
+ for (const row of rows) {
21514
+ const canonicalName = getMapped(row, fieldMap, "canonical_name");
21515
+ if (!canonicalName) {
21516
+ errors.push("Row missing canonical_name");
21517
+ continue;
21518
+ }
21519
+ const website = getMapped(row, fieldMap, "canonical_domain") ?? "";
21520
+ const canonicalDomain = stripUrl(website);
21521
+ await insertOrganization({
21522
+ canonical_name: canonicalName,
21523
+ canonical_domain: canonicalDomain || null,
21524
+ source_system: sourceSystem,
21525
+ source_id: getMapped(row, fieldMap, "source_id") ?? "",
21526
+ raw_data: row,
21527
+ metadata: buildMetadata(row, mappings)
21528
+ });
21529
+ imported++;
21530
+ }
21531
+ return { imported, errors };
21532
+ }
21533
+ async function importPeople(params) {
21534
+ const { rows, mappings, sourceSystem } = params;
21535
+ const errors = [];
21536
+ let imported = 0;
21537
+ const fieldMap = reverseMap(mappings);
21538
+ const orgSourceIds = /* @__PURE__ */ new Set();
21539
+ for (const row of rows) {
21540
+ const orgSrcId = getMapped(row, fieldMap, "organization_source_id");
21541
+ if (orgSrcId) orgSourceIds.add(orgSrcId);
21542
+ }
21543
+ const orgIdMap = await resolveSourceIds("organizations", sourceSystem, orgSourceIds);
21544
+ for (const row of rows) {
21545
+ const firstName = getMapped(row, fieldMap, "first_name") ?? "";
21546
+ const lastName = getMapped(row, fieldMap, "last_name") ?? "";
21547
+ const canonicalName = [firstName, lastName].filter(Boolean).join(" ") || getMapped(row, fieldMap, "canonical_name") || "";
21548
+ if (!canonicalName) {
21549
+ errors.push("Row missing name fields");
21550
+ continue;
21551
+ }
21552
+ const orgSourceId = getMapped(row, fieldMap, "organization_source_id");
21553
+ await insertPerson({
21554
+ canonical_name: canonicalName,
21555
+ canonical_email: getMapped(row, fieldMap, "canonical_email") || null,
21556
+ organization_id: orgSourceId ? orgIdMap.get(orgSourceId) ?? null : null,
21557
+ source_system: sourceSystem,
21558
+ source_id: getMapped(row, fieldMap, "source_id") ?? "",
21559
+ raw_data: row,
21560
+ metadata: buildMetadata(row, mappings)
21561
+ });
21562
+ imported++;
21563
+ }
21564
+ return { imported, errors };
21565
+ }
21566
+ async function importOpportunities(params) {
21567
+ const { rows, mappings, sourceSystem } = params;
21568
+ const errors = [];
21569
+ let imported = 0;
21570
+ const fieldMap = reverseMap(mappings);
21571
+ const orgSourceIds = /* @__PURE__ */ new Set();
21572
+ const ownerSourceIds = /* @__PURE__ */ new Set();
21573
+ for (const row of rows) {
21574
+ const o = getMapped(row, fieldMap, "organization_source_id");
21575
+ if (o) orgSourceIds.add(o);
21576
+ const w = getMapped(row, fieldMap, "owner_source_id");
21577
+ if (w) ownerSourceIds.add(w);
21578
+ }
21579
+ const orgIdMap = await resolveSourceIds("organizations", sourceSystem, orgSourceIds);
21580
+ const ownerIdMap = await resolveSourceIds("people", sourceSystem, ownerSourceIds);
21581
+ for (const row of rows) {
21582
+ const canonicalName = getMapped(row, fieldMap, "canonical_name");
21583
+ if (!canonicalName) {
21584
+ errors.push("Row missing canonical_name");
21585
+ continue;
21586
+ }
21587
+ const orgSourceId = getMapped(row, fieldMap, "organization_source_id");
21588
+ const ownerSourceId = getMapped(row, fieldMap, "owner_source_id");
21589
+ const amountStr = getMapped(row, fieldMap, "amount");
21590
+ await insertOpportunity({
21591
+ canonical_name: canonicalName,
21592
+ organization_id: orgSourceId ? orgIdMap.get(orgSourceId) ?? null : null,
21593
+ owner_id: ownerSourceId ? ownerIdMap.get(ownerSourceId) ?? null : null,
21594
+ current_stage: getMapped(row, fieldMap, "current_stage") || null,
21595
+ amount: amountStr ? parseFloat(amountStr) || null : null,
21596
+ close_date: getMapped(row, fieldMap, "close_date") || null,
21597
+ source_system: sourceSystem,
21598
+ source_id: getMapped(row, fieldMap, "source_id") ?? "",
21599
+ raw_data: row,
21600
+ metadata: buildMetadata(row, mappings)
21601
+ });
21602
+ imported++;
21603
+ }
21604
+ return { imported, errors };
21605
+ }
21606
+ async function importActivities(params) {
21607
+ const { rows, mappings, sourceSystem } = params;
21608
+ const errors = [];
21609
+ let imported = 0;
21610
+ const fieldMap = reverseMap(mappings);
21611
+ const personSourceIds = /* @__PURE__ */ new Set();
21612
+ for (const row of rows) {
21613
+ const p = getMapped(row, fieldMap, "person_source_id");
21614
+ if (p) personSourceIds.add(p);
21615
+ }
21616
+ const personIdMap = await resolveSourceIds("people", sourceSystem, personSourceIds);
21617
+ for (const row of rows) {
21618
+ const rawType = getMapped(row, fieldMap, "activity_type") ?? "custom";
21619
+ const activityType = ACTIVITY_TYPE_MAP[rawType] ?? "custom";
21620
+ const rawOccurredAt = getMapped(row, fieldMap, "occurred_at") ?? "";
21621
+ const occurredAt = parseTimestamp(rawOccurredAt);
21622
+ if (!occurredAt) {
21623
+ errors.push("Row missing or invalid occurred_at");
21624
+ continue;
21625
+ }
21626
+ const personSourceId = getMapped(row, fieldMap, "person_source_id");
21627
+ await insertActivity({
21628
+ activity_type: activityType,
21629
+ occurred_at: occurredAt,
21630
+ person_id: personSourceId ? personIdMap.get(personSourceId) ?? null : null,
21631
+ organization_id: null,
21632
+ opportunity_id: null,
21633
+ source_system: sourceSystem,
21634
+ source_id: getMapped(row, fieldMap, "source_id") ?? "",
21635
+ raw_data: row,
21636
+ metadata: buildMetadata(row, mappings)
21637
+ });
21638
+ imported++;
21639
+ }
21640
+ return { imported, errors };
21641
+ }
21642
+ async function importCampaigns(params) {
21643
+ const { rows, mappings, sourceSystem } = params;
21644
+ const errors = [];
21645
+ let imported = 0;
21646
+ const fieldMap = reverseMap(mappings);
21647
+ for (const row of rows) {
21648
+ const canonicalName = getMapped(row, fieldMap, "canonical_name");
21649
+ if (!canonicalName) {
21650
+ errors.push("Row missing canonical_name");
21651
+ continue;
21652
+ }
21653
+ const rawType = row["sequence_type"] ?? "";
21654
+ const campaignType = CAMPAIGN_TYPE_MAP[rawType] ?? "email_sequence";
21655
+ await insertCampaign({
21656
+ canonical_name: canonicalName,
21657
+ campaign_type: campaignType,
21658
+ source_system: sourceSystem,
21659
+ source_id: getMapped(row, fieldMap, "source_id") ?? "",
21660
+ raw_data: row,
21661
+ metadata: buildMetadata(row, mappings)
21662
+ });
21663
+ imported++;
21664
+ }
21665
+ return { imported, errors };
21666
+ }
21667
+ var init_importer = __esm({
21668
+ "src/pipeline/importer.ts"() {
21669
+ "use strict";
21670
+ init_csv_mappings();
21671
+ init_queries();
21672
+ init_connection();
21673
+ }
21674
+ });
21675
+
21676
+ // src/pipeline/resolver.ts
21677
+ async function resolveIdentities() {
21678
+ const orgsResolved = await resolveOrganizations();
21679
+ const peopleResolved = await resolvePeople();
21680
+ return { resolved: orgsResolved + peopleResolved, peopleResolved, orgsResolved };
21681
+ }
21682
+ async function resolveOrganizations() {
21683
+ let resolved = 0;
21684
+ const orgs = await all(
21685
+ `SELECT id, canonical_domain, created_at FROM organizations WHERE canonical_id IS NULL AND canonical_domain IS NOT NULL ORDER BY created_at ASC`
21686
+ );
21687
+ const groups = /* @__PURE__ */ new Map();
21688
+ for (const org of orgs) {
21689
+ const key = org.canonical_domain.toLowerCase();
21690
+ const group = groups.get(key);
21691
+ if (group) group.push(org);
21692
+ else groups.set(key, [org]);
21693
+ }
21694
+ for (const [, group] of groups) {
21695
+ if (group.length <= 1) continue;
21696
+ const canonical = group[0];
21697
+ const duplicateIds = group.slice(1).map((d) => d.id);
21698
+ const placeholders = duplicateIds.map(() => "?").join(", ");
21699
+ await run(`UPDATE organizations SET canonical_id = ? WHERE id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21700
+ await run(`UPDATE people SET organization_id = ? WHERE organization_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21701
+ await run(`UPDATE opportunities SET organization_id = ? WHERE organization_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21702
+ await run(`UPDATE activities SET organization_id = ? WHERE organization_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21703
+ resolved += duplicateIds.length;
21704
+ }
21705
+ return resolved;
21706
+ }
21707
+ async function resolvePeople() {
21708
+ let resolved = 0;
21709
+ const people = await all(
21710
+ `SELECT id, canonical_email, organization_id, created_at FROM people WHERE canonical_id IS NULL AND canonical_email IS NOT NULL ORDER BY created_at ASC`
21711
+ );
21712
+ const groups = /* @__PURE__ */ new Map();
21713
+ for (const person of people) {
21714
+ const key = person.canonical_email.toLowerCase();
21715
+ const group = groups.get(key);
21716
+ if (group) group.push(person);
21717
+ else groups.set(key, [person]);
21718
+ }
21719
+ for (const [, group] of groups) {
21720
+ if (group.length <= 1) continue;
21721
+ const canonical = group[0];
21722
+ const duplicates = group.slice(1);
21723
+ const duplicateIds = duplicates.map((d) => d.id);
21724
+ const placeholders = duplicateIds.map(() => "?").join(", ");
21725
+ if (!canonical.organization_id) {
21726
+ const withOrg = duplicates.find((d) => d.organization_id);
21727
+ if (withOrg) {
21728
+ await run(`UPDATE people SET organization_id = ? WHERE id = ?`, [withOrg.organization_id, canonical.id]);
21546
21729
  }
21547
- };
21548
- SCENARIO_LIST = Object.values(SCENARIOS);
21549
- NAMED_DEMO_SCENARIOS = [
21550
- "hidden_crisis",
21551
- "leaky_bucket",
21552
- "stale_pipeline",
21553
- "lone_wolf",
21554
- "busy_bees",
21555
- "even_keel",
21556
- "compound_pain"
21557
- ];
21558
- RANDOM_POOL = [...NAMED_DEMO_SCENARIOS];
21730
+ }
21731
+ await run(`UPDATE people SET canonical_id = ? WHERE id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21732
+ await run(`UPDATE activities SET person_id = ? WHERE person_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21733
+ await run(`UPDATE opportunities SET owner_id = ? WHERE owner_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21734
+ resolved += duplicateIds.length;
21735
+ }
21736
+ return resolved;
21737
+ }
21738
+ var init_resolver2 = __esm({
21739
+ "src/pipeline/resolver.ts"() {
21740
+ "use strict";
21741
+ init_connection();
21742
+ }
21743
+ });
21744
+
21745
+ // src/demo/seed.ts
21746
+ function mulberry32(seed) {
21747
+ let a = seed | 0;
21748
+ return () => {
21749
+ a = a + 1831565813 | 0;
21750
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
21751
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
21752
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
21753
+ };
21754
+ }
21755
+ function createSeededRandom(seed) {
21756
+ const raw = mulberry32(seed);
21757
+ const rng = {
21758
+ next: raw,
21759
+ nextInt(min, max) {
21760
+ return Math.floor(raw() * (max - min + 1)) + min;
21761
+ },
21762
+ nextFloat(min, max) {
21763
+ return raw() * (max - min) + min;
21764
+ },
21765
+ pick(arr) {
21766
+ if (arr.length === 0) {
21767
+ throw new Error("Cannot pick from an empty array");
21768
+ }
21769
+ return arr[Math.floor(raw() * arr.length)];
21770
+ },
21771
+ pickN(arr, n) {
21772
+ const copy = [...arr];
21773
+ rng.shuffle(copy);
21774
+ return copy.slice(0, Math.min(n, copy.length));
21775
+ },
21776
+ shuffle(arr) {
21777
+ for (let i = arr.length - 1; i > 0; i--) {
21778
+ const j = Math.floor(raw() * (i + 1));
21779
+ const current = arr[i];
21780
+ arr[i] = arr[j];
21781
+ arr[j] = current;
21782
+ }
21783
+ return arr;
21784
+ },
21785
+ chance(probability) {
21786
+ return raw() < probability;
21787
+ },
21788
+ uuid() {
21789
+ const bytes = Array.from({ length: 16 }, () => Math.floor(raw() * 256));
21790
+ bytes[6] = bytes[6] & 15 | 64;
21791
+ bytes[8] = bytes[8] & 63 | 128;
21792
+ const hex = bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
21793
+ return [
21794
+ hex.slice(0, 8),
21795
+ hex.slice(8, 12),
21796
+ hex.slice(12, 16),
21797
+ hex.slice(16, 20),
21798
+ hex.slice(20, 32)
21799
+ ].join("-");
21800
+ },
21801
+ date(start, end) {
21802
+ const s = start.getTime();
21803
+ const e = end.getTime();
21804
+ return new Date(s + raw() * (e - s));
21805
+ },
21806
+ weightedPick(items, weights) {
21807
+ if (items.length === 0) {
21808
+ throw new Error("Cannot pick from an empty weighted item list");
21809
+ }
21810
+ const total = weights.reduce((sum, w) => sum + w, 0);
21811
+ let r = raw() * total;
21812
+ for (let i = 0; i < items.length; i++) {
21813
+ r -= weights[i] ?? 0;
21814
+ if (r <= 0) return items[i];
21815
+ }
21816
+ return items[items.length - 1];
21817
+ }
21818
+ };
21819
+ return rng;
21820
+ }
21821
+ var init_seed = __esm({
21822
+ "src/demo/seed.ts"() {
21823
+ "use strict";
21559
21824
  }
21560
21825
  });
21561
21826
 
@@ -23542,320 +23807,93 @@ async function insertDirect(dataset) {
23542
23807
  metadata: opp.metadata,
23543
23808
  created_at: createdAt
23544
23809
  });
23545
- oppIdMap.set(opp.localId, dbId);
23546
- }
23547
- for (const act of dataset.activities) {
23548
- await insertActivity({
23549
- activity_type: act.activity_type,
23550
- occurred_at: act.occurred_at,
23551
- person_id: act.personLocalId ? personIdMap.get(act.personLocalId) ?? null : null,
23552
- organization_id: act.organizationLocalId ? orgIdMap.get(act.organizationLocalId) ?? null : null,
23553
- opportunity_id: act.opportunityLocalId ? oppIdMap.get(act.opportunityLocalId) ?? null : null,
23554
- source_system: act.source_system,
23555
- source_id: act.source_id,
23556
- raw_data: act.raw_data,
23557
- metadata: act.metadata
23558
- });
23559
- }
23560
- for (const campaign of dataset.campaigns) {
23561
- await insertCampaign({
23562
- canonical_name: campaign.canonical_name,
23563
- campaign_type: campaign.campaign_type,
23564
- source_system: campaign.source_system,
23565
- source_id: campaign.source_id,
23566
- raw_data: campaign.raw_data,
23567
- metadata: campaign.metadata
23568
- });
23569
- }
23570
- for (const event of dataset.revenueEvents ?? []) {
23571
- await insertRevenueEvent({
23572
- organization_id: orgIdMap.get(event.organizationLocalId) ?? null,
23573
- period: event.period,
23574
- amount: event.amount,
23575
- event_type: event.event_type,
23576
- source_system: event.source_system,
23577
- source_id: event.source_id,
23578
- raw_data: event.raw_data
23579
- });
23580
- }
23581
- const { invalidateLexiconSeed: invalidateLexiconSeed2 } = await Promise.resolve().then(() => (init_lexicon_seed(), lexicon_seed_exports));
23582
- invalidateLexiconSeed2();
23583
- return {
23584
- mode: "direct",
23585
- counts: {
23586
- organizations: dataset.organizations.length,
23587
- people: dataset.people.length,
23588
- opportunities: dataset.opportunities.length,
23589
- activities: dataset.activities.length,
23590
- campaigns: dataset.campaigns.length,
23591
- revenue_events: dataset.revenueEvents?.length ?? 0
23592
- },
23593
- health: null
23594
- };
23595
- }
23596
- function hashString(str2) {
23597
- let hash = 0;
23598
- for (let i = 0; i < str2.length; i++) {
23599
- const char = str2.charCodeAt(i);
23600
- hash = (hash << 5) - hash + char | 0;
23601
- }
23602
- return Math.abs(hash);
23603
- }
23604
- var init_generator = __esm({
23605
- "src/demo/generator.ts"() {
23606
- "use strict";
23607
- init_queries();
23608
- init_seed();
23609
- init_scenarios();
23610
- init_organizations();
23611
- init_people();
23612
- init_opportunities();
23613
- init_activities();
23614
- init_saas_enrichment();
23615
- init_salesforce();
23616
- init_hubspot();
23617
- init_outreach();
23618
- }
23619
- });
23620
-
23621
- // src/demo/scenario-fit.ts
23622
- function dealBandFromCycleDays(days) {
23623
- if (days == null || !Number.isFinite(days) || days <= 0) return void 0;
23624
- if (days <= 21) return "velocity";
23625
- if (days <= 45) return "core";
23626
- if (days <= 90) return "mid";
23627
- return "enterprise";
23628
- }
23629
- function dealBandFromAverageDealSize(raw) {
23630
- if (!raw) return void 0;
23631
- const t = raw.trim().toLowerCase();
23632
- if (!t) return void 0;
23633
- const match = t.match(/(\d+(?:\.\d+)?)\s*(k|m|million|thousand)?/i);
23634
- if (!match) return void 0;
23635
- let n = Number(match[1]);
23636
- if (!Number.isFinite(n)) return void 0;
23637
- const unit = (match[2] ?? "").toLowerCase();
23638
- if (unit === "k" || unit === "thousand") n *= 1e3;
23639
- else if (unit === "m" || unit === "million") n *= 1e6;
23640
- else if (n > 0 && n < 500) n *= 1e3;
23641
- if (n < 15e3) return "velocity";
23642
- if (n < 5e4) return "core";
23643
- if (n < 15e4) return "mid";
23644
- return "enterprise";
23645
- }
23646
- function signalsFromProfile(profile) {
23647
- if (!profile) return {};
23648
- const text = [
23649
- profile.industry,
23650
- profile.product_description,
23651
- profile.target_customer,
23652
- profile.user_scope,
23653
- profile.custom_context
23654
- ].filter(Boolean).join("\n");
23655
- return {
23656
- salesMotion: profile.sales_motion,
23657
- dealBand: dealBandFromAverageDealSize(profile.average_deal_size) ?? dealBandFromCycleDays(profile.sales_cycle_days),
23658
- cycleDays: profile.sales_cycle_days,
23659
- text
23660
- };
23661
- }
23662
- function keywordHits(text) {
23663
- const hits = {};
23664
- const hay = text.toLowerCase();
23665
- for (const { scenario, patterns } of KEYWORD_HINTS) {
23666
- let n = 0;
23667
- for (const re of patterns) {
23668
- if (re.test(hay)) n++;
23669
- }
23670
- if (n > 0) hits[scenario] = n;
23671
- }
23672
- return hits;
23673
- }
23674
- function matrixPick(motion, band) {
23675
- if (motion && band) return MATRIX[motion][band];
23676
- if (motion) return MOTION_ONLY[motion];
23677
- if (band) return BAND_ONLY[band];
23678
- return "even_keel";
23679
- }
23680
- function reasonFor(scenario, signals, via) {
23681
- const s = getScenario(scenario);
23682
- if (via === "keywords") {
23683
- return `Your notes sound like ${s.label} \u2014 ${s.hook}`;
23810
+ oppIdMap.set(opp.localId, dbId);
23684
23811
  }
23685
- const motion = signals.salesMotion;
23686
- const band = signals.dealBand;
23687
- if (motion && band) {
23688
- return `${labelMotion(motion)} with ${labelBand(band)} deals maps to ${s.label}.`;
23812
+ for (const act of dataset.activities) {
23813
+ await insertActivity({
23814
+ activity_type: act.activity_type,
23815
+ occurred_at: act.occurred_at,
23816
+ person_id: act.personLocalId ? personIdMap.get(act.personLocalId) ?? null : null,
23817
+ organization_id: act.organizationLocalId ? orgIdMap.get(act.organizationLocalId) ?? null : null,
23818
+ opportunity_id: act.opportunityLocalId ? oppIdMap.get(act.opportunityLocalId) ?? null : null,
23819
+ source_system: act.source_system,
23820
+ source_id: act.source_id,
23821
+ raw_data: act.raw_data,
23822
+ metadata: act.metadata
23823
+ });
23689
23824
  }
23690
- if (motion) return `${labelMotion(motion)} books usually show up as ${s.label}.`;
23691
- if (band) return `${labelBand(band)} deals usually show up as ${s.label}.`;
23692
- return `${s.label} is the even-keeled starting book when we don't know the motion yet.`;
23693
- }
23694
- function labelMotion(m) {
23695
- return MOTION_FIT_CHOICES.find((c) => c.value === m)?.label ?? m;
23696
- }
23697
- function labelBand(b) {
23698
- return DEAL_BAND_CHOICES.find((c) => c.value === b)?.label ?? b;
23699
- }
23700
- function inferDemoScenario(signals) {
23701
- const text = signals.text?.trim() ?? "";
23702
- if (text) {
23703
- const hits = keywordHits(text);
23704
- let best;
23705
- let bestN = 0;
23706
- for (const id of NAMED_DEMO_SCENARIOS) {
23707
- const n = hits[id] ?? 0;
23708
- if (n > bestN) {
23709
- best = id;
23710
- bestN = n;
23711
- }
23712
- }
23713
- if (best && bestN > 0) {
23714
- return { scenario: best, reason: reasonFor(best, signals, "keywords"), source: "heuristic" };
23715
- }
23825
+ for (const campaign of dataset.campaigns) {
23826
+ await insertCampaign({
23827
+ canonical_name: campaign.canonical_name,
23828
+ campaign_type: campaign.campaign_type,
23829
+ source_system: campaign.source_system,
23830
+ source_id: campaign.source_id,
23831
+ raw_data: campaign.raw_data,
23832
+ metadata: campaign.metadata
23833
+ });
23716
23834
  }
23717
- const scenario = matrixPick(signals.salesMotion, signals.dealBand);
23718
- return { scenario, reason: reasonFor(scenario, signals, "matrix"), source: "heuristic" };
23719
- }
23720
- function getPreferredDemoScenario() {
23721
- const raw = getConfigValue(PREF_SCENARIO_KEY);
23722
- return raw && isNamedDemoScenario(raw) ? raw : void 0;
23723
- }
23724
- function saveDemoFit(opts) {
23725
- setConfigValue(PREF_SCENARIO_KEY, opts.scenario);
23726
- if (opts.motion) {
23727
- setConfigValue(PREF_MOTION_KEY, opts.motion);
23728
- if (opts.syncSalesMotion && !isProfileConfigured(loadProfile())) {
23729
- setConfigValue("sales-motion", opts.motion);
23730
- }
23835
+ for (const event of dataset.revenueEvents ?? []) {
23836
+ await insertRevenueEvent({
23837
+ organization_id: orgIdMap.get(event.organizationLocalId) ?? null,
23838
+ period: event.period,
23839
+ amount: event.amount,
23840
+ event_type: event.event_type,
23841
+ source_system: event.source_system,
23842
+ source_id: event.source_id,
23843
+ raw_data: event.raw_data
23844
+ });
23731
23845
  }
23732
- if (opts.dealBand) setConfigValue(PREF_BAND_KEY, opts.dealBand);
23846
+ const { invalidateLexiconSeed: invalidateLexiconSeed2 } = await Promise.resolve().then(() => (init_lexicon_seed(), lexicon_seed_exports));
23847
+ invalidateLexiconSeed2();
23848
+ return {
23849
+ mode: "direct",
23850
+ counts: {
23851
+ organizations: dataset.organizations.length,
23852
+ people: dataset.people.length,
23853
+ opportunities: dataset.opportunities.length,
23854
+ activities: dataset.activities.length,
23855
+ campaigns: dataset.campaigns.length,
23856
+ revenue_events: dataset.revenueEvents?.length ?? 0
23857
+ },
23858
+ health: null
23859
+ };
23733
23860
  }
23734
- function scenarioMenuChoices() {
23735
- return NAMED_DEMO_SCENARIOS.map((id) => {
23736
- const s = getScenario(id);
23737
- return {
23738
- value: id,
23739
- label: s.label,
23740
- description: s.hook
23741
- };
23742
- });
23861
+ function hashString(str2) {
23862
+ let hash = 0;
23863
+ for (let i = 0; i < str2.length; i++) {
23864
+ const char = str2.charCodeAt(i);
23865
+ hash = (hash << 5) - hash + char | 0;
23866
+ }
23867
+ return Math.abs(hash);
23743
23868
  }
23744
- var PREF_SCENARIO_KEY, PREF_MOTION_KEY, PREF_BAND_KEY, MOTION_FIT_CHOICES, DEAL_BAND_CHOICES, MATRIX, MOTION_ONLY, BAND_ONLY, KEYWORD_HINTS;
23745
- var init_scenario_fit = __esm({
23746
- "src/demo/scenario-fit.ts"() {
23869
+ var init_generator = __esm({
23870
+ "src/demo/generator.ts"() {
23747
23871
  "use strict";
23748
- init_store();
23749
- init_profile();
23872
+ init_queries();
23873
+ init_seed();
23750
23874
  init_scenarios();
23751
- PREF_SCENARIO_KEY = "demo-scenario-preference";
23752
- PREF_MOTION_KEY = "demo-fit-motion";
23753
- PREF_BAND_KEY = "demo-fit-deal-band";
23754
- MOTION_FIT_CHOICES = [
23755
- {
23756
- value: "plg",
23757
- label: "Product-led / self-serve",
23758
- description: "Users start themselves; sales assists or expands"
23759
- },
23760
- {
23761
- value: "smb_velocity",
23762
- label: "High-volume SMB",
23763
- description: "Fast cycles, lots of small deals, outbound or inbound machine"
23764
- },
23765
- {
23766
- value: "mid_market",
23767
- label: "Mid-market, structured process",
23768
- description: "A real sales cycle, a few stakeholders, moderate ACV"
23769
- },
23770
- {
23771
- value: "enterprise",
23772
- label: "Enterprise, long cycles",
23773
- description: "Large deals, many buyers, quarters not weeks"
23774
- }
23775
- ];
23776
- DEAL_BAND_CHOICES = [
23777
- {
23778
- value: "velocity",
23779
- label: "Under ~$15K, days to a couple of weeks",
23780
- description: "Velocity / transactional"
23781
- },
23782
- {
23783
- value: "core",
23784
- label: "~$15K\u2013$50K, a few weeks",
23785
- description: "Core SMB"
23786
- },
23787
- {
23788
- value: "mid",
23789
- label: "~$50K\u2013$150K, 1\u20133 months",
23790
- description: "Classic mid-market"
23791
- },
23792
- {
23793
- value: "enterprise",
23794
- label: "$150K+, a quarter or more",
23795
- description: "Enterprise / strategic"
23796
- }
23797
- ];
23798
- MATRIX = {
23799
- plg: {
23800
- velocity: "leaky_bucket",
23801
- core: "leaky_bucket",
23802
- mid: "hidden_crisis",
23803
- enterprise: "hidden_crisis"
23804
- },
23805
- smb_velocity: {
23806
- velocity: "busy_bees",
23807
- core: "busy_bees",
23808
- mid: "leaky_bucket",
23809
- enterprise: "lone_wolf"
23810
- },
23811
- mid_market: {
23812
- velocity: "busy_bees",
23813
- core: "stale_pipeline",
23814
- mid: "stale_pipeline",
23815
- enterprise: "hidden_crisis"
23816
- },
23817
- enterprise: {
23818
- velocity: "lone_wolf",
23819
- core: "lone_wolf",
23820
- mid: "hidden_crisis",
23821
- enterprise: "hidden_crisis"
23822
- }
23823
- };
23824
- MOTION_ONLY = {
23825
- plg: "leaky_bucket",
23826
- smb_velocity: "busy_bees",
23827
- mid_market: "stale_pipeline",
23828
- enterprise: "hidden_crisis"
23829
- };
23830
- BAND_ONLY = {
23831
- velocity: "busy_bees",
23832
- core: "leaky_bucket",
23833
- mid: "stale_pipeline",
23834
- enterprise: "hidden_crisis"
23835
- };
23836
- KEYWORD_HINTS = [
23837
- { scenario: "even_keel", patterns: [/\beven keel\b/, /\breasonably healthy\b/, /\bno crisis\b/, /\bjust evaluating\b/, /\bgreen[- ]field\b/] },
23838
- { scenario: "compound_pain", patterns: [/\beverything('?s| is) (on fire|red|broken)\b/, /\bcompound\b/, /\ball (five )?vitals\b/, /\bmultiple problems\b/] },
23839
- { scenario: "leaky_bucket", patterns: [/\bhandoff\b/, /\bmqls?\b/, /\bleak/, /\bdrop[- ]rate/, /\bvanish/, /\brouting\b/, /\bmarketing.?sales\b/] },
23840
- { scenario: "stale_pipeline", patterns: [/\bzombie/, /\bstale\b/, /\bpast[- ]due\b/, /\bforecast(ing)? on fiction\b/, /\bstuck in negotiation\b/, /\bquiet deals?\b/] },
23841
- { scenario: "lone_wolf", patterns: [/\bsingle[- ]thread/, /\blone wolf\b/, /\bone contact\b/, /\bchampion leaves\b/] },
23842
- { scenario: "busy_bees", patterns: [/\bspray/, /\bnois(e|y)\b/, /\bmisdirected\b/, /\bactivity (volume|metrics)\b/, /\bbusy bees\b/, /\bnot (on|hitting) pipeline\b/] },
23843
- { scenario: "hidden_crisis", patterns: [/\bhidden crisis\b/, /\benterprise (is )?(dying|red|stale)\b/, /\bsegment.{0,20}mask/, /\baverages? (look|looks) (fine|okay|yellow)\b/] }
23844
- ];
23875
+ init_organizations();
23876
+ init_people();
23877
+ init_opportunities();
23878
+ init_activities();
23879
+ init_saas_enrichment();
23880
+ init_salesforce();
23881
+ init_hubspot();
23882
+ init_outreach();
23845
23883
  }
23846
23884
  });
23847
23885
 
23848
23886
  // src/demo/taxonomy-cache.ts
23849
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync15, existsSync as existsSync22, mkdirSync as mkdirSync13, unlinkSync as unlinkSync3 } from "fs";
23887
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync15, existsSync as existsSync23, mkdirSync as mkdirSync13, unlinkSync as unlinkSync3 } from "fs";
23850
23888
  import { homedir as homedir7 } from "os";
23851
23889
  import { join as join24 } from "path";
23852
23890
  function ensureDir6() {
23853
- if (!existsSync22(NTRP_DIR4)) {
23891
+ if (!existsSync23(NTRP_DIR4)) {
23854
23892
  mkdirSync13(NTRP_DIR4, { recursive: true });
23855
23893
  }
23856
23894
  }
23857
23895
  function loadCachedTaxonomy(profile) {
23858
- if (!existsSync22(TAXONOMY_PATH)) return null;
23896
+ if (!existsSync23(TAXONOMY_PATH)) return null;
23859
23897
  try {
23860
23898
  const parsed = JSON.parse(readFileSync19(TAXONOMY_PATH, "utf-8"));
23861
23899
  if (!parsed || typeof parsed !== "object") return null;
@@ -24332,7 +24370,7 @@ __export(inbox_setup_exports, {
24332
24370
  shouldOfferInboxSkillSetup: () => shouldOfferInboxSkillSetup
24333
24371
  });
24334
24372
  import chalk27 from "chalk";
24335
- import { existsSync as existsSync23 } from "fs";
24373
+ import { existsSync as existsSync24 } from "fs";
24336
24374
  function markDemoOffered() {
24337
24375
  setConfigValue("ai-inbox-nudge-seen", "true");
24338
24376
  }
@@ -24364,7 +24402,7 @@ function printSkipHint(beat) {
24364
24402
  async function reuseInboxFolderIfPresent(session, beat, folderPath) {
24365
24403
  if (getAiInboxDir()) return false;
24366
24404
  const candidates = folderPath ? [folderPath] : [.../* @__PURE__ */ new Set([defaultAiInboxDir(), legacyAiInboxDir()])];
24367
- const existing = candidates.find((p) => existsSync23(p));
24405
+ const existing = candidates.find((p) => existsSync24(p));
24368
24406
  if (!existing) return false;
24369
24407
  console.log(" " + chalk27.dim("Pickup folder still on disk: ") + existing);
24370
24408
  const reuse = await session.confirm("Reuse this pickup folder?", true);
@@ -24470,7 +24508,7 @@ __export(ingest_exports, {
24470
24508
  handler: () => handler3
24471
24509
  });
24472
24510
  import chalk28 from "chalk";
24473
- import { readFileSync as readFileSync20, existsSync as existsSync24 } from "fs";
24511
+ import { readFileSync as readFileSync20, existsSync as existsSync25 } from "fs";
24474
24512
  import { basename as basename6 } from "path";
24475
24513
  async function handler3(args, ctx) {
24476
24514
  const { positional, flags } = parseArgs(args, [
@@ -24494,7 +24532,7 @@ async function handler3(args, ctx) {
24494
24532
  console.error(chalk28.dim(" /ingest --demo [--scenario <name>]"));
24495
24533
  process.exit(1);
24496
24534
  }
24497
- if (!existsSync24(file)) {
24535
+ if (!existsSync25(file)) {
24498
24536
  console.error(chalk28.red(` File not found: ${file}`));
24499
24537
  process.exit(1);
24500
24538
  }
@@ -24876,39 +24914,54 @@ var ingest_chat_exports = {};
24876
24914
  __export(ingest_chat_exports, {
24877
24915
  extractFilePath: () => extractFilePath,
24878
24916
  ingestFromChat: () => ingestFromChat,
24917
+ ingestPathFromChat: () => ingestPathFromChat,
24879
24918
  isDemoIntent: () => isDemoIntent,
24919
+ listCsvsInFolder: () => listCsvsInFolder,
24880
24920
  loadDemoFromChat: () => loadDemoFromChat,
24881
24921
  looksLikeFilePath: () => looksLikeFilePath
24882
24922
  });
24883
- import { existsSync as existsSync25 } from "fs";
24884
- import { basename as basename7, resolve as resolve9 } from "path";
24923
+ import { existsSync as existsSync26, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
24924
+ import { basename as basename7, join as join25, resolve as resolve9 } from "path";
24885
24925
  import { homedir as homedir8 } from "os";
24886
24926
  import chalk30 from "chalk";
24887
24927
  function extractFilePath(input) {
24888
- const trimmed = input.trim();
24928
+ const trimmed = input.trim().replace(/^["']|["']$/g, "");
24929
+ if (!trimmed) return null;
24889
24930
  const patterns = [
24890
- /^["'](.+\.csv)["']$/i,
24891
- /^@(.+\.csv)$/i,
24892
- /(?:here'?s|file|path|upload)[:\s]+["']?([^\s"']+\.csv)["']?/i,
24893
- /^~\/\S+\.csv$/i,
24894
- /^\.\.?\/\S+\.csv$/i,
24895
- /^\/\S+\.csv$/i,
24896
- /^[A-Za-z]:\\[^\s]+\.csv$/i,
24897
- /^[^\s]+\.csv$/i
24931
+ /^["'](.+)["']$/i,
24932
+ /^@(.+)$/i,
24933
+ /(?:here'?s|file|path|upload|folder|dir)[:\s]+["']?([^\s"']+)["']?/i,
24934
+ /^~\/\S+$/i,
24935
+ /^\.\.?\/\S+$/i,
24936
+ /^\/\S+$/i,
24937
+ /^[A-Za-z]:\\[^\s]+$/i,
24938
+ /^[^\s]+$/i
24898
24939
  ];
24899
24940
  for (const re of patterns) {
24900
24941
  const m = trimmed.match(re);
24901
- if (m?.[1]) {
24902
- const p = expandPath(m[1]);
24903
- if (existsSync25(p)) return p;
24904
- }
24905
- if (!m?.[1] && re.test(trimmed) && trimmed.toLowerCase().endsWith(".csv")) {
24906
- const p = expandPath(trimmed.replace(/^["']|["']$/g, ""));
24907
- if (existsSync25(p)) return p;
24942
+ const candidate = (m?.[1] ?? (re.test(trimmed) ? trimmed : null))?.replace(/^["']|["']$/g, "");
24943
+ if (!candidate) continue;
24944
+ if (!looksLikePathToken(candidate)) continue;
24945
+ const p = expandPath(candidate);
24946
+ if (existsSync26(p)) {
24947
+ try {
24948
+ const st = statSync5(p);
24949
+ if (st.isFile() || st.isDirectory()) return p;
24950
+ } catch {
24951
+ }
24908
24952
  }
24909
24953
  }
24910
24954
  return null;
24911
24955
  }
24956
+ function looksLikePathToken(token) {
24957
+ if (token.length < 2) return false;
24958
+ if (/^(yes|no|y|n|back|b|prev|cancel|skip|help|demo)$/i.test(token)) return false;
24959
+ if (token.includes("/") || token.includes("\\")) return true;
24960
+ if (token.startsWith("~")) return true;
24961
+ if (/^[A-Za-z]:/.test(token)) return true;
24962
+ if (/\.[A-Za-z0-9]{1,8}$/.test(token)) return true;
24963
+ return false;
24964
+ }
24912
24965
  function expandPath(p) {
24913
24966
  if (p.startsWith("~/")) return resolve9(homedir8(), p.slice(2));
24914
24967
  return resolve9(p);
@@ -24916,6 +24969,73 @@ function expandPath(p) {
24916
24969
  function looksLikeFilePath(input) {
24917
24970
  return extractFilePath(input) !== null;
24918
24971
  }
24972
+ function listCsvsInFolder(dir) {
24973
+ try {
24974
+ if (!statSync5(dir).isDirectory()) return [];
24975
+ return readdirSync6(dir).filter((name) => name.toLowerCase().endsWith(".csv")).map((name) => join25(dir, name)).sort();
24976
+ } catch {
24977
+ return [];
24978
+ }
24979
+ }
24980
+ async function ingestPathFromChat(ctx, rawPath) {
24981
+ let st;
24982
+ try {
24983
+ st = statSync5(rawPath);
24984
+ } catch {
24985
+ console.log(" " + chalk30.red(`Path not found: ${rawPath}`));
24986
+ return false;
24987
+ }
24988
+ if (st.isDirectory()) {
24989
+ const csvs = listCsvsInFolder(rawPath);
24990
+ if (csvs.length === 0) {
24991
+ console.log();
24992
+ console.log(" " + paint("accent", "Folder noted") + chalk30.dim(` \u2014 ${rawPath}`));
24993
+ console.log(" " + chalk30.dim("No CSV files one level deep. Drop a .csv path, or put exports in that folder."));
24994
+ ctx.attachments = [
24995
+ ...ctx.attachments ?? [],
24996
+ { path: rawPath, ingested_at: (/* @__PURE__ */ new Date()).toISOString() }
24997
+ ];
24998
+ saveSessionState(ctx);
24999
+ markProductionDataSeen();
25000
+ return false;
25001
+ }
25002
+ if (csvs.length === 1) {
25003
+ console.log(" " + chalk30.dim(`Found ${basename7(csvs[0])} in folder \u2014 ingesting.`));
25004
+ return ingestFromChat(ctx, csvs[0]);
25005
+ }
25006
+ if (!ctx.rl) {
25007
+ console.log(" " + chalk30.dim(`Found ${csvs.length} CSVs \u2014 re-run interactively to pick one.`));
25008
+ return false;
25009
+ }
25010
+ const prompts = createPromptSession(ctx.rl, ctx);
25011
+ try {
25012
+ const picked = await prompts.choose(
25013
+ "Which CSV in that folder?",
25014
+ csvs.map((p) => ({ value: p, label: basename7(p), description: p })),
25015
+ { default: csvs[0] }
25016
+ );
25017
+ return ingestFromChat(ctx, picked);
25018
+ } finally {
25019
+ prompts.close();
25020
+ }
25021
+ }
25022
+ if (st.isFile()) {
25023
+ if (!rawPath.toLowerCase().endsWith(".csv")) {
25024
+ console.log();
25025
+ console.log(" " + paint("accent", "Path noted") + chalk30.dim(` \u2014 ${rawPath}`));
25026
+ console.log(" " + chalk30.dim("NTRP ingests CSV exports today. Drop a .csv from that location when ready."));
25027
+ ctx.attachments = [
25028
+ ...ctx.attachments ?? [],
25029
+ { path: rawPath, ingested_at: (/* @__PURE__ */ new Date()).toISOString() }
25030
+ ];
25031
+ saveSessionState(ctx);
25032
+ markProductionDataSeen();
25033
+ return false;
25034
+ }
25035
+ return ingestFromChat(ctx, rawPath);
25036
+ }
25037
+ return false;
25038
+ }
24919
25039
  async function ingestFromChat(ctx, filePath) {
24920
25040
  if (!ctx.rl) {
24921
25041
  console.log(" " + chalk30.red("Ingest confirm requires interactive mode."));
@@ -24980,6 +25100,7 @@ async function ingestFromChat(ctx, filePath) {
24980
25100
  };
24981
25101
  invalidateGapAudit(ctx);
24982
25102
  saveSessionState(ctx);
25103
+ markProductionDataSeen();
24983
25104
  console.log();
24984
25105
  console.log(" " + paint("accent", "\u2713 Data loaded") + chalk30.dim(` \u2014 ${name}`));
24985
25106
  recordMessage(ctx, "user", `[ingested ${name}]`);
@@ -25055,6 +25176,7 @@ async function loadDemoFromChat(ctx, scenario, opts = {}) {
25055
25176
  counts,
25056
25177
  ingested_at: (/* @__PURE__ */ new Date()).toISOString()
25057
25178
  };
25179
+ markDemoDataSeen();
25058
25180
  if (!ctx.scope) {
25059
25181
  const { proposeScopeFromIntent: proposeScopeFromIntent2 } = await Promise.resolve().then(() => (init_scope(), scope_exports));
25060
25182
  const proposal = proposeScopeFromIntent2("demo pipeline and metrics");
@@ -25091,6 +25213,7 @@ var init_ingest_chat = __esm({
25091
25213
  init_compute2();
25092
25214
  init_theme();
25093
25215
  init_demo();
25216
+ init_onboard_tiers();
25094
25217
  }
25095
25218
  });
25096
25219
 
@@ -25849,9 +25972,9 @@ async function handleGetSessionBrief(input) {
25849
25972
  if (!target) {
25850
25973
  return { error: `No session matching "${raw}".` };
25851
25974
  }
25852
- const { existsSync: existsSync28, readFileSync: readFileSync23 } = await import("fs");
25975
+ const { existsSync: existsSync29, readFileSync: readFileSync23 } = await import("fs");
25853
25976
  const briefPath = contextDocPathForSession2(target.id);
25854
- if (!existsSync28(briefPath)) {
25977
+ if (!existsSync29(briefPath)) {
25855
25978
  return {
25856
25979
  session_id: target.id,
25857
25980
  error: "No context brief on disk for this session (created before brief storage existed).",
@@ -26866,14 +26989,14 @@ var init_ask = __esm({
26866
26989
  });
26867
26990
 
26868
26991
  // src/services/setup.ts
26869
- import { existsSync as existsSync26, mkdirSync as mkdirSync14, readFileSync as readFileSync21, writeFileSync as writeFileSync16 } from "fs";
26870
- import { join as join25 } from "path";
26992
+ import { existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync21, writeFileSync as writeFileSync16 } from "fs";
26993
+ import { join as join26 } from "path";
26871
26994
  function setupCheck() {
26872
26995
  const home = ntrpHome();
26873
26996
  let writable = false;
26874
26997
  try {
26875
26998
  mkdirSync14(home, { recursive: true });
26876
- const probe = join25(home, ".write-check");
26999
+ const probe = join26(home, ".write-check");
26877
27000
  writeFileSync16(probe, "ok\n");
26878
27001
  writable = true;
26879
27002
  } catch {
@@ -26925,11 +27048,11 @@ var init_setup = __esm({
26925
27048
  });
26926
27049
 
26927
27050
  // src/version.ts
26928
- import { existsSync as existsSync27, readFileSync as readFileSync22 } from "fs";
26929
- import { dirname as dirname5, join as join26 } from "path";
27051
+ import { existsSync as existsSync28, readFileSync as readFileSync22 } from "fs";
27052
+ import { dirname as dirname5, join as join27 } from "path";
26930
27053
  import { fileURLToPath } from "url";
26931
27054
  function readVersionFromPackageJson(packageJsonPath) {
26932
- if (!existsSync27(packageJsonPath)) return null;
27055
+ if (!existsSync28(packageJsonPath)) return null;
26933
27056
  try {
26934
27057
  const pkg = JSON.parse(readFileSync22(packageJsonPath, "utf-8"));
26935
27058
  if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
@@ -26939,7 +27062,7 @@ function readVersionFromPackageJson(packageJsonPath) {
26939
27062
  }
26940
27063
  function readVersionNearEntry(entryPath) {
26941
27064
  const start = dirname5(entryPath);
26942
- for (const rel of [join26(start, "..", "package.json"), join26(start, "../..", "package.json")]) {
27065
+ for (const rel of [join27(start, "..", "package.json"), join27(start, "../..", "package.json")]) {
26943
27066
  const version = readVersionFromPackageJson(rel);
26944
27067
  if (version) return version;
26945
27068
  }