@sonnechasser/ntrp 1.4.8 → 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 +867 -191
  2. package/dist/mcp/server.js +1145 -1008
  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
  {
@@ -10317,6 +10321,7 @@ CLI slide deck: SaaS refresher, five vital signs (definition, formula, visual, d
10317
10321
  Bare \`/deepdive\` runs the full onboarding tour. Type \`/deepdive guide\` to jump to how to use NTRP (includes teaching the inbox once).
10318
10322
  Type \`/deepdive handoff\` for the ship-to-AI slide. Type \`/deepdive <metric>\` to jump to one metric card.
10319
10323
  Type \`/deepdive list\` to print the catalog. Works without an AI key. Re-run anytime from the homescreen.
10324
+ During the tour: Enter advances, \`b\` / \`back\` goes to the previous slide, \`q\` quits. After the tour, the same \`b\` / \`back\` word steps back one confirm gate in the REPL (scope, data, strategy) \u2014 it does not unload data or undo compute.
10320
10325
  Live values overlay when an analysis exists.`
10321
10326
  },
10322
10327
  {
@@ -10331,7 +10336,7 @@ handler: ../commands/thinkwithme.ts
10331
10336
 
10332
10337
  Open a dedicated \`think \u203A\` channel to explore ideas, challenge assumptions, and pull evidence from your data.
10333
10338
  Bare \`/thinkwithme\` enters the channel. Type \`/thinkwithme <topic>\` to seed the first turn.
10334
- Requires an analysis and an AI key (\`/connect\`). Type \`done\` or \`cancel\` to return to \u203A.
10339
+ Requires an analysis and an AI key (\`/connect\`). Type \`done\`, \`cancel\`, or \`b\` / \`back\` to return to \u203A.
10335
10340
  When you are ready to commit to a plan, say so or the partner can hand off to \`/strategy\`.`
10336
10341
  },
10337
10342
  {
@@ -13242,10 +13247,12 @@ var init_agent_context = __esm({
13242
13247
  var scope_exports = {};
13243
13248
  __export(scope_exports, {
13244
13249
  confirmScope: () => confirmScope,
13250
+ isBackInput: () => isBackInput,
13245
13251
  isScopeAdjustInput: () => isScopeAdjustInput,
13246
13252
  isScopeConfirmInput: () => isScopeConfirmInput,
13247
13253
  isSkipClarifier: () => isSkipClarifier,
13248
- proposeScopeFromIntent: () => proposeScopeFromIntent
13254
+ proposeScopeFromIntent: () => proposeScopeFromIntent,
13255
+ unconfirmScope: () => unconfirmScope
13249
13256
  });
13250
13257
  function proposeScopeFromIntent(input) {
13251
13258
  const text = input.trim();
@@ -13290,12 +13297,20 @@ function confirmScope(ctx) {
13290
13297
  if (ctx.scope.primary_lens === "revenue_metrics" || ctx.scope.primary_lens === "gtm_health") {
13291
13298
  }
13292
13299
  }
13300
+ function unconfirmScope(ctx) {
13301
+ if (!ctx.scope?.confirmed_at) return;
13302
+ const { confirmed_at: _dropped, ...rest } = ctx.scope;
13303
+ ctx.scope = rest;
13304
+ }
13293
13305
  function isScopeConfirmInput(input) {
13294
13306
  return /^(yes|y|yeah|yep|correct|looks good|that'?s right|confirm|ok|okay|go)$/i.test(input.trim());
13295
13307
  }
13296
13308
  function isScopeAdjustInput(input) {
13297
13309
  return /^(adjust|change|refine|edit|no|nope)$/i.test(input.trim());
13298
13310
  }
13311
+ function isBackInput(input) {
13312
+ return /^(b|back|prev)\s*[.!]?\s*$/i.test(input.trim());
13313
+ }
13299
13314
  function isSkipClarifier(input) {
13300
13315
  return /^skip$/i.test(input.trim());
13301
13316
  }
@@ -14246,7 +14261,8 @@ function buildConversationPrompt(ctx) {
14246
14261
  const enterHint2 = action ? chalk8.dim(` \xB7 \u23CE ${action.hint}`) : "";
14247
14262
  return accentPromptHead(phase, sessionName) + chalk8.dim(stack) + enterHint2 + " ";
14248
14263
  }
14249
- const enterHint = action ? chalk8.dim(`\u23CE ${action.hint} `) : "";
14264
+ const modalBack = phase === "scope" || phase === "awaiting_data" || phase === "strategize" ? " \xB7 b back" : "";
14265
+ const enterHint = action ? chalk8.dim(`\u23CE ${action.hint}${modalBack} `) : "";
14250
14266
  return accentPromptHead(phase, sessionName) + enterHint;
14251
14267
  }
14252
14268
  var PROMPT_LABELS;
@@ -18837,7 +18853,7 @@ async function handleStrategizeFlow(input, ctx) {
18837
18853
  " " + chalk17.dim("That looks like a question. A strategy objective is waiting.")
18838
18854
  );
18839
18855
  console.log(
18840
- " " + chalk17.dim("Type ") + chalk17.cyan("yes") + chalk17.dim(" to make the plan. Type ") + chalk17.cyan("adjust") + chalk17.dim(" to change the objective. Type ") + chalk17.cyan("cancel") + chalk17.dim(" to answer questions first.")
18856
+ " " + chalk17.dim("Type ") + chalk17.cyan("yes") + chalk17.dim(" to make the plan. Type ") + chalk17.cyan("b") + chalk17.dim(" / ") + chalk17.cyan("adjust") + chalk17.dim(" to change the objective. Type ") + chalk17.cyan("cancel") + chalk17.dim(" to answer questions first.")
18841
18857
  );
18842
18858
  console.log();
18843
18859
  return "Awaiting confirm";
@@ -18850,7 +18866,7 @@ async function handleStrategizeFlow(input, ctx) {
18850
18866
  }
18851
18867
  console.log();
18852
18868
  console.log(
18853
- " " + chalk17.dim("Type ") + chalk17.cyan("yes") + chalk17.dim(" to make the plan. Type ") + chalk17.cyan("adjust") + chalk17.dim(" to change the objective. Type ") + chalk17.cyan("cancel") + chalk17.dim(" to stop.")
18869
+ " " + chalk17.dim("Type ") + chalk17.cyan("yes") + chalk17.dim(" to make the plan. Type ") + chalk17.cyan("b") + chalk17.dim(" / ") + chalk17.cyan("adjust") + chalk17.dim(" to change the objective. Type ") + chalk17.cyan("cancel") + chalk17.dim(" to stop.")
18854
18870
  );
18855
18871
  console.log();
18856
18872
  return "Awaiting confirm";
@@ -19027,7 +19043,7 @@ function printObjectiveCard(ctx, objective, proposed) {
19027
19043
  );
19028
19044
  console.log();
19029
19045
  console.log(
19030
- " " + chalk17.dim("Confirm? ") + chalk17.cyan("\u23CE yes") + chalk17.dim(" \xB7 ") + chalk17.cyan("adjust") + chalk17.dim(" \xB7 ") + chalk17.cyan("cancel")
19046
+ " " + chalk17.dim("Confirm? ") + chalk17.cyan("\u23CE yes") + chalk17.dim(" \xB7 ") + chalk17.cyan("b back") + chalk17.dim(" \xB7 ") + chalk17.cyan("adjust") + chalk17.dim(" \xB7 ") + chalk17.cyan("cancel")
19031
19047
  );
19032
19048
  console.log();
19033
19049
  }
@@ -19170,7 +19186,7 @@ function buildSituationalAwarenessBlock(ctx, opts = {}) {
19170
19186
  `Phase: ${phase} (${formatPhaseLabel(phase)}) \u2014 ${PHASE_MEANING[phase]}`
19171
19187
  ];
19172
19188
  if (channel === "think") {
19173
- lines.push("Channel: think \u2014 socratic partner; type done/cancel returns to \u203A");
19189
+ lines.push("Channel: think \u2014 socratic partner; type done/cancel/b back returns to \u203A");
19174
19190
  lines.push("Response mode: deep (tools on)");
19175
19191
  } else if (channel === "strategist") {
19176
19192
  lines.push("Channel: strategist \u2014 measurable plan engine owns sequencing");
@@ -19214,11 +19230,13 @@ function buildSituationalAwarenessBlock(ctx, opts = {}) {
19214
19230
  } else if (phase === "awaiting_data") {
19215
19231
  lines.push("Already done: scope confirmed; analysis not ready");
19216
19232
  lines.push(
19217
- 'Available now: tell the user to paste a CSV path, type "use demo data", or \u23CE go ahead when can_compute \u2014 you cannot open files'
19233
+ 'Available now: tell the user to paste a CSV path, type "use demo data", \u23CE go ahead when can_compute, or b/back to rewind the focus card \u2014 you cannot open files'
19218
19234
  );
19219
19235
  } else if (phase === "scope") {
19220
19236
  lines.push("Already done: intent proposed; waiting on confirm");
19221
- lines.push("Available now: wait for CLI confirm (\u23CE yes) \u2014 do not invent a different question");
19237
+ lines.push(
19238
+ "Available now: wait for CLI confirm (\u23CE yes) or b/back/adjust \u2014 do not invent a different question"
19239
+ );
19222
19240
  } else if (phase === "orient") {
19223
19241
  lines.push("Already done: nothing locked yet");
19224
19242
  lines.push("Available now: help the user name a focus; CLI will propose scope from their words");
@@ -19291,13 +19309,13 @@ var init_situation = __esm({
19291
19309
  FUNNEL_SPINE = "Funnel: orient \u2192 scope \u2192 data \u2192 compute \u2192 explore | side doors: think, strategy, ship";
19292
19310
  PHASE_MEANING = {
19293
19311
  orient: "setup \u2014 open chat; user has not locked a scope yet",
19294
- scope: "scope confirm \u2014 CLI owns yes/adjust; do not re-propose unless asked",
19295
- awaiting_data: "data gate \u2014 load CSV/demo or compute; CLI owns the gate",
19312
+ scope: "scope confirm \u2014 CLI owns yes / b back / adjust; do not re-propose unless asked",
19313
+ awaiting_data: "data gate \u2014 load CSV/demo or compute; b/back rewinds to scope confirm (does not unload data); CLI owns the gate",
19296
19314
  compute: "compute in progress \u2014 wait",
19297
19315
  explore: "analysis complete \u2014 free-form Q&A on this session's artifact",
19298
- think: "socratic think channel \u2014 return to \u203A via done/cancel",
19299
- strategize: "strategist flow \u2014 objective confirm or plan drafting",
19300
- deliver: "ship/handoff wizard \u2014 CLI owns write confirms"
19316
+ think: "socratic think channel \u2014 return to \u203A via done/cancel/b back",
19317
+ strategize: "strategist flow \u2014 objective confirm (yes / b back / adjust) or plan drafting",
19318
+ deliver: "ship/handoff wizard \u2014 CLI owns write confirms; b/back or cancel leaves the draft"
19301
19319
  };
19302
19320
  }
19303
19321
  });
@@ -20824,724 +20842,985 @@ var init_demo = __esm({
20824
20842
  }
20825
20843
  });
20826
20844
 
20827
- // src/pipeline/csv-parse.ts
20828
- var csv_parse_exports = {};
20829
- __export(csv_parse_exports, {
20830
- getPreviewRows: () => getPreviewRows,
20831
- 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
20832
20856
  });
20833
- import Papa from "papaparse";
20834
- function parseCSV(content) {
20835
- const result = Papa.parse(content, {
20836
- header: true,
20837
- skipEmptyLines: true,
20838
- transformHeader: (h) => h.trim()
20839
- });
20840
- return {
20841
- headers: result.meta.fields ?? [],
20842
- rows: result.data,
20843
- rowCount: result.data.length
20844
- };
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;
20845
20863
  }
20846
- function getPreviewRows(rows, count = 5) {
20847
- return rows.slice(0, count);
20864
+ function isNamedDemoScenario(raw) {
20865
+ return NAMED_DEMO_SCENARIOS.includes(raw);
20848
20866
  }
20849
- var init_csv_parse = __esm({
20850
- "src/pipeline/csv-parse.ts"() {
20851
- "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;
20852
20873
  }
20853
- });
20854
-
20855
- // src/pipeline/csv-mappings.ts
20856
- function getDefaultMappings(sourceSystem, entityType) {
20857
- 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;
20858
20879
  }
20859
- var DEFAULT_MAPPINGS, ACTIVITY_TYPE_MAP, CAMPAIGN_TYPE_MAP;
20860
- var init_csv_mappings = __esm({
20861
- "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"() {
20862
20905
  "use strict";
20863
- DEFAULT_MAPPINGS = {
20864
- salesforce: {
20865
- organizations: { Name: "canonical_name", Website: "canonical_domain", Id: "source_id" },
20866
- people: { Email: "canonical_email", FirstName: "first_name", LastName: "last_name", Id: "source_id", AccountId: "organization_source_id" },
20867
- opportunities: { Name: "canonical_name", Amount: "amount", StageName: "current_stage", CloseDate: "close_date", Id: "source_id", AccountId: "organization_source_id", OwnerId: "owner_source_id" },
20868
- 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
20869
20943
  },
20870
- hubspot: {
20871
- organizations: { "Company name": "canonical_name", "Company Domain Name": "canonical_domain", "Company ID": "source_id" },
20872
- people: { Email: "canonical_email", "First Name": "first_name", "Last Name": "last_name", "Contact ID": "source_id", "Associated Company ID": "organization_source_id" },
20873
- 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
20874
20954
  },
20875
- outreach: {
20876
- campaigns: { name: "canonical_name", id: "source_id" },
20877
- 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
20878
21020
  }
20879
21021
  };
20880
- ACTIVITY_TYPE_MAP = {
20881
- Call: "call",
20882
- Email: "email",
20883
- Task: "custom",
20884
- CALL: "call",
20885
- EMAIL: "email",
20886
- MEETING: "meeting",
20887
- NOTE: "custom",
20888
- TASK: "custom",
20889
- INCOMING_EMAIL: "email",
20890
- FORWARDED_EMAIL: "email",
20891
- call: "call",
20892
- email: "email",
20893
- sequence_step: "email"
20894
- };
20895
- CAMPAIGN_TYPE_MAP = {
20896
- outbound: "email_sequence",
20897
- inbound: "email_sequence",
20898
- trigger: "email_sequence"
20899
- };
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];
20900
21033
  }
20901
21034
  });
20902
21035
 
20903
- // src/pipeline/csv-detect.ts
20904
- var csv_detect_exports = {};
20905
- __export(csv_detect_exports, {
20906
- detectEntityType: () => detectEntityType
20907
- });
20908
- function detectEntityType(headers, sourceSystem) {
20909
- const rules = SIGNATURES[sourceSystem];
20910
- if (!rules) return null;
20911
- const headerSet = new Set(headers);
20912
- let bestMatch = null;
20913
- for (const rule of rules) {
20914
- const requiredMatches = rule.required.filter((col) => headerSet.has(col)).length;
20915
- const requiredTotal = rule.required.length;
20916
- if (requiredMatches < Math.ceil(requiredTotal / 2)) continue;
20917
- const optionalMatches = rule.optional.filter((col) => headerSet.has(col)).length;
20918
- const optionalTotal = rule.optional.length;
20919
- const score = requiredMatches / requiredTotal * 0.7 + (optionalTotal > 0 ? optionalMatches / optionalTotal * 0.3 : 0.3);
20920
- if (!bestMatch || score > bestMatch.score) {
20921
- bestMatch = { rule, score };
20922
- }
20923
- }
20924
- if (!bestMatch) return null;
20925
- const defaultMappings = getDefaultMappings(sourceSystem, bestMatch.rule.entityType);
20926
- const mappings = {};
20927
- for (const [csvCol, ontologyField] of Object.entries(defaultMappings)) {
20928
- if (headerSet.has(csvCol)) {
20929
- mappings[csvCol] = ontologyField;
20930
- }
20931
- }
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");
20932
21070
  return {
20933
- entityType: bestMatch.rule.entityType,
20934
- confidence: Math.round(bestMatch.score * 100) / 100,
20935
- 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
20936
21075
  };
20937
21076
  }
20938
- var SIGNATURES;
20939
- var init_csv_detect = __esm({
20940
- "src/pipeline/csv-detect.ts"() {
20941
- "use strict";
20942
- init_csv_mappings();
20943
- SIGNATURES = {
20944
- salesforce: [
20945
- { entityType: "organizations", required: ["Name", "Industry"], optional: ["AnnualRevenue", "BillingCountry", "Website", "NumberOfEmployees"], weight: 1 },
20946
- { entityType: "people", required: ["FirstName", "LastName", "Email"], optional: ["AccountId", "Title", "Phone"], weight: 1 },
20947
- { entityType: "opportunities", required: ["StageName", "Amount", "CloseDate"], optional: ["AccountId", "OwnerId", "Probability"], weight: 1 },
20948
- { entityType: "activities", required: ["Subject", "ActivityDate", "TaskSubtype"], optional: ["WhoId", "WhatId", "Status"], weight: 1 }
20949
- ],
20950
- hubspot: [
20951
- { entityType: "organizations", required: ["Company name", "Company Domain Name"], optional: ["Company ID", "Industry", "Annual Revenue"], weight: 1 },
20952
- { entityType: "people", required: ["First Name", "Last Name", "Email"], optional: ["Contact ID", "Associated Company ID", "Lifecycle Stage"], weight: 1 },
20953
- { entityType: "activities", required: ["Engagement ID", "Type"], optional: ["Timestamp", "Created At", "Contact ID"], weight: 1 }
20954
- ],
20955
- outreach: [
20956
- { entityType: "campaigns", required: ["sequence_type", "step_count"], optional: ["name", "id", "enabled"], weight: 1 },
20957
- { entityType: "activities", required: ["prospect_id", "prospect_email"], optional: ["type", "created_at", "subject"], weight: 1 }
20958
- ]
20959
- };
20960
- }
20961
- });
20962
-
20963
- // src/pipeline/importer.ts
20964
- async function importRows(params) {
20965
- let result;
20966
- switch (params.entityType) {
20967
- case "organizations":
20968
- result = await importOrganizations(params);
20969
- break;
20970
- case "people":
20971
- result = await importPeople(params);
20972
- break;
20973
- case "opportunities":
20974
- result = await importOpportunities(params);
20975
- break;
20976
- case "activities":
20977
- result = await importActivities(params);
20978
- break;
20979
- case "campaigns":
20980
- result = await importCampaigns(params);
20981
- break;
20982
- default:
20983
- result = { imported: 0, errors: [`Unknown entity type: ${params.entityType}`] };
20984
- }
20985
- const { invalidateLexiconSeed: invalidateLexiconSeed2 } = await Promise.resolve().then(() => (init_lexicon_seed(), lexicon_seed_exports));
20986
- invalidateLexiconSeed2();
20987
- return result;
20988
- }
20989
- function reverseMap(mappings) {
20990
- const reversed = {};
20991
- for (const [csvCol, ontologyField] of Object.entries(mappings)) {
20992
- 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;
20993
21086
  }
20994
- return reversed;
21087
+ return hits;
20995
21088
  }
20996
- function getMapped(row, fieldMap, field) {
20997
- const csvCol = fieldMap[field];
20998
- if (!csvCol) return void 0;
20999
- const val = row[csvCol];
21000
- 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";
21001
21094
  }
21002
- function buildMetadata(row, mappings) {
21003
- const mappedCols = new Set(Object.keys(mappings));
21004
- const metadata = {};
21005
- for (const [key, value] of Object.entries(row)) {
21006
- if (!mappedCols.has(key) && value !== void 0 && value !== "") {
21007
- metadata[key] = value;
21008
- }
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}`;
21009
21099
  }
21010
- 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.`;
21011
21108
  }
21012
- function stripUrl(url) {
21013
- return url.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
21109
+ function labelMotion(m) {
21110
+ return MOTION_FIT_CHOICES.find((c) => c.value === m)?.label ?? m;
21014
21111
  }
21015
- function parseTimestamp(value) {
21016
- if (!value) return null;
21017
- const num2 = Number(value);
21018
- if (!isNaN(num2) && num2 > 1e12) return new Date(num2).toISOString();
21019
- if (!isNaN(num2) && num2 > 1e9) return new Date(num2 * 1e3).toISOString();
21020
- const d = new Date(value);
21021
- if (!isNaN(d.getTime())) return d.toISOString();
21022
- return null;
21112
+ function labelBand(b) {
21113
+ return DEAL_BAND_CHOICES.find((c) => c.value === b)?.label ?? b;
21023
21114
  }
21024
- async function resolveSourceIds(table, sourceSystem, sourceIds) {
21025
- const idMap = /* @__PURE__ */ new Map();
21026
- if (sourceIds.size === 0) return idMap;
21027
- const ids = Array.from(sourceIds);
21028
- const placeholders = ids.map(() => "?").join(", ");
21029
- const rows = await all(
21030
- `SELECT id, source_id FROM ${table} WHERE source_system = ? AND source_id IN (${placeholders})`,
21031
- [sourceSystem, ...ids]
21032
- );
21033
- for (const row of rows) {
21034
- 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
+ }
21035
21131
  }
21036
- return idMap;
21132
+ const scenario = matrixPick(signals.salesMotion, signals.dealBand);
21133
+ return { scenario, reason: reasonFor(scenario, signals, "matrix"), source: "heuristic" };
21037
21134
  }
21038
- async function importOrganizations(params) {
21039
- const { rows, mappings, sourceSystem } = params;
21040
- const errors = [];
21041
- let imported = 0;
21042
- const fieldMap = reverseMap(mappings);
21043
- for (const row of rows) {
21044
- const canonicalName = getMapped(row, fieldMap, "canonical_name");
21045
- if (!canonicalName) {
21046
- errors.push("Row missing canonical_name");
21047
- 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);
21048
21145
  }
21049
- const website = getMapped(row, fieldMap, "canonical_domain") ?? "";
21050
- const canonicalDomain = stripUrl(website);
21051
- await insertOrganization({
21052
- canonical_name: canonicalName,
21053
- canonical_domain: canonicalDomain || null,
21054
- source_system: sourceSystem,
21055
- source_id: getMapped(row, fieldMap, "source_id") ?? "",
21056
- raw_data: row,
21057
- metadata: buildMetadata(row, mappings)
21058
- });
21059
- imported++;
21060
21146
  }
21061
- return { imported, errors };
21147
+ if (opts.dealBand) setConfigValue(PREF_BAND_KEY, opts.dealBand);
21062
21148
  }
21063
- async function importPeople(params) {
21064
- const { rows, mappings, sourceSystem } = params;
21065
- const errors = [];
21066
- let imported = 0;
21067
- const fieldMap = reverseMap(mappings);
21068
- const orgSourceIds = /* @__PURE__ */ new Set();
21069
- for (const row of rows) {
21070
- const orgSrcId = getMapped(row, fieldMap, "organization_source_id");
21071
- if (orgSrcId) orgSourceIds.add(orgSrcId);
21072
- }
21073
- const orgIdMap = await resolveSourceIds("organizations", sourceSystem, orgSourceIds);
21074
- for (const row of rows) {
21075
- const firstName = getMapped(row, fieldMap, "first_name") ?? "";
21076
- const lastName = getMapped(row, fieldMap, "last_name") ?? "";
21077
- const canonicalName = [firstName, lastName].filter(Boolean).join(" ") || getMapped(row, fieldMap, "canonical_name") || "";
21078
- if (!canonicalName) {
21079
- errors.push("Row missing name fields");
21080
- continue;
21081
- }
21082
- const orgSourceId = getMapped(row, fieldMap, "organization_source_id");
21083
- await insertPerson({
21084
- canonical_name: canonicalName,
21085
- canonical_email: getMapped(row, fieldMap, "canonical_email") || null,
21086
- organization_id: orgSourceId ? orgIdMap.get(orgSourceId) ?? null : null,
21087
- source_system: sourceSystem,
21088
- source_id: getMapped(row, fieldMap, "source_id") ?? "",
21089
- raw_data: row,
21090
- metadata: buildMetadata(row, mappings)
21091
- });
21092
- imported++;
21093
- }
21094
- 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
+ });
21095
21158
  }
21096
- async function importOpportunities(params) {
21097
- const { rows, mappings, sourceSystem } = params;
21098
- const errors = [];
21099
- let imported = 0;
21100
- const fieldMap = reverseMap(mappings);
21101
- const orgSourceIds = /* @__PURE__ */ new Set();
21102
- const ownerSourceIds = /* @__PURE__ */ new Set();
21103
- for (const row of rows) {
21104
- const o = getMapped(row, fieldMap, "organization_source_id");
21105
- if (o) orgSourceIds.add(o);
21106
- const w = getMapped(row, fieldMap, "owner_source_id");
21107
- if (w) ownerSourceIds.add(w);
21108
- }
21109
- const orgIdMap = await resolveSourceIds("organizations", sourceSystem, orgSourceIds);
21110
- const ownerIdMap = await resolveSourceIds("people", sourceSystem, ownerSourceIds);
21111
- for (const row of rows) {
21112
- const canonicalName = getMapped(row, fieldMap, "canonical_name");
21113
- if (!canonicalName) {
21114
- errors.push("Row missing canonical_name");
21115
- continue;
21116
- }
21117
- const orgSourceId = getMapped(row, fieldMap, "organization_source_id");
21118
- const ownerSourceId = getMapped(row, fieldMap, "owner_source_id");
21119
- const amountStr = getMapped(row, fieldMap, "amount");
21120
- await insertOpportunity({
21121
- canonical_name: canonicalName,
21122
- organization_id: orgSourceId ? orgIdMap.get(orgSourceId) ?? null : null,
21123
- owner_id: ownerSourceId ? ownerIdMap.get(ownerSourceId) ?? null : null,
21124
- current_stage: getMapped(row, fieldMap, "current_stage") || null,
21125
- amount: amountStr ? parseFloat(amountStr) || null : null,
21126
- close_date: getMapped(row, fieldMap, "close_date") || null,
21127
- source_system: sourceSystem,
21128
- source_id: getMapped(row, fieldMap, "source_id") ?? "",
21129
- raw_data: row,
21130
- metadata: buildMetadata(row, mappings)
21131
- });
21132
- 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
+ ];
21133
21260
  }
21134
- 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]));
21135
21267
  }
21136
- async function importActivities(params) {
21137
- const { rows, mappings, sourceSystem } = params;
21138
- const errors = [];
21139
- let imported = 0;
21140
- const fieldMap = reverseMap(mappings);
21141
- const personSourceIds = /* @__PURE__ */ new Set();
21142
- for (const row of rows) {
21143
- const p = getMapped(row, fieldMap, "person_source_id");
21144
- if (p) personSourceIds.add(p);
21145
- }
21146
- const personIdMap = await resolveSourceIds("people", sourceSystem, personSourceIds);
21147
- for (const row of rows) {
21148
- const rawType = getMapped(row, fieldMap, "activity_type") ?? "custom";
21149
- const activityType = ACTIVITY_TYPE_MAP[rawType] ?? "custom";
21150
- const rawOccurredAt = getMapped(row, fieldMap, "occurred_at") ?? "";
21151
- const occurredAt = parseTimestamp(rawOccurredAt);
21152
- if (!occurredAt) {
21153
- errors.push("Row missing or invalid occurred_at");
21154
- continue;
21155
- }
21156
- const personSourceId = getMapped(row, fieldMap, "person_source_id");
21157
- await insertActivity({
21158
- activity_type: activityType,
21159
- occurred_at: occurredAt,
21160
- person_id: personSourceId ? personIdMap.get(personSourceId) ?? null : null,
21161
- organization_id: null,
21162
- opportunity_id: null,
21163
- source_system: sourceSystem,
21164
- source_id: getMapped(row, fieldMap, "source_id") ?? "",
21165
- raw_data: row,
21166
- metadata: buildMetadata(row, mappings)
21167
- });
21168
- 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);
21169
21272
  }
21170
- return { imported, errors };
21171
21273
  }
21172
- async function importCampaigns(params) {
21173
- const { rows, mappings, sourceSystem } = params;
21174
- const errors = [];
21175
- let imported = 0;
21176
- const fieldMap = reverseMap(mappings);
21177
- for (const row of rows) {
21178
- const canonicalName = getMapped(row, fieldMap, "canonical_name");
21179
- if (!canonicalName) {
21180
- errors.push("Row missing canonical_name");
21181
- continue;
21182
- }
21183
- const rawType = row["sequence_type"] ?? "";
21184
- const campaignType = CAMPAIGN_TYPE_MAP[rawType] ?? "email_sequence";
21185
- await insertCampaign({
21186
- canonical_name: canonicalName,
21187
- campaign_type: campaignType,
21188
- source_system: sourceSystem,
21189
- source_id: getMapped(row, fieldMap, "source_id") ?? "",
21190
- raw_data: row,
21191
- metadata: buildMetadata(row, mappings)
21192
- });
21193
- imported++;
21194
- }
21195
- return { imported, errors };
21274
+ function markProductionDataSeen() {
21275
+ markOnboardTierComplete("production");
21196
21276
  }
21197
- var init_importer = __esm({
21198
- "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"() {
21199
21283
  "use strict";
21200
- init_csv_mappings();
21201
- init_queries();
21202
- 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
+ };
21203
21294
  }
21204
21295
  });
21205
21296
 
21206
- // src/pipeline/resolver.ts
21207
- async function resolveIdentities() {
21208
- const orgsResolved = await resolveOrganizations();
21209
- const peopleResolved = await resolvePeople();
21210
- 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
+ };
21211
21315
  }
21212
- async function resolveOrganizations() {
21213
- let resolved = 0;
21214
- const orgs = await all(
21215
- `SELECT id, canonical_domain, created_at FROM organizations WHERE canonical_id IS NULL AND canonical_domain IS NOT NULL ORDER BY created_at ASC`
21216
- );
21217
- const groups = /* @__PURE__ */ new Map();
21218
- for (const org of orgs) {
21219
- const key = org.canonical_domain.toLowerCase();
21220
- const group = groups.get(key);
21221
- if (group) group.push(org);
21222
- else groups.set(key, [org]);
21223
- }
21224
- for (const [, group] of groups) {
21225
- if (group.length <= 1) continue;
21226
- const canonical = group[0];
21227
- const duplicateIds = group.slice(1).map((d) => d.id);
21228
- const placeholders = duplicateIds.map(() => "?").join(", ");
21229
- await run(`UPDATE organizations SET canonical_id = ? WHERE id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21230
- await run(`UPDATE people SET organization_id = ? WHERE organization_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21231
- await run(`UPDATE opportunities SET organization_id = ? WHERE organization_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21232
- await run(`UPDATE activities SET organization_id = ? WHERE organization_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21233
- resolved += duplicateIds.length;
21234
- }
21235
- return resolved;
21236
- }
21237
- async function resolvePeople() {
21238
- let resolved = 0;
21239
- const people = await all(
21240
- `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`
21241
- );
21242
- const groups = /* @__PURE__ */ new Map();
21243
- for (const person of people) {
21244
- const key = person.canonical_email.toLowerCase();
21245
- const group = groups.get(key);
21246
- if (group) group.push(person);
21247
- else groups.set(key, [person]);
21248
- }
21249
- for (const [, group] of groups) {
21250
- if (group.length <= 1) continue;
21251
- const canonical = group[0];
21252
- const duplicates = group.slice(1);
21253
- const duplicateIds = duplicates.map((d) => d.id);
21254
- const placeholders = duplicateIds.map(() => "?").join(", ");
21255
- if (!canonical.organization_id) {
21256
- const withOrg = duplicates.find((d) => d.organization_id);
21257
- if (withOrg) {
21258
- await run(`UPDATE people SET organization_id = ? WHERE id = ?`, [withOrg.organization_id, canonical.id]);
21259
- }
21260
- }
21261
- await run(`UPDATE people SET canonical_id = ? WHERE id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21262
- await run(`UPDATE activities SET person_id = ? WHERE person_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21263
- await run(`UPDATE opportunities SET owner_id = ? WHERE owner_id IN (${placeholders})`, [canonical.id, ...duplicateIds]);
21264
- resolved += duplicateIds.length;
21265
- }
21266
- return resolved;
21316
+ function getPreviewRows(rows, count = 5) {
21317
+ return rows.slice(0, count);
21267
21318
  }
21268
- var init_resolver2 = __esm({
21269
- "src/pipeline/resolver.ts"() {
21319
+ var init_csv_parse = __esm({
21320
+ "src/pipeline/csv-parse.ts"() {
21270
21321
  "use strict";
21271
- init_connection();
21272
21322
  }
21273
21323
  });
21274
21324
 
21275
- // src/demo/seed.ts
21276
- function mulberry32(seed) {
21277
- let a = seed | 0;
21278
- return () => {
21279
- a = a + 1831565813 | 0;
21280
- let t = Math.imul(a ^ a >>> 15, 1 | a);
21281
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
21282
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
21283
- };
21284
- }
21285
- function createSeededRandom(seed) {
21286
- const raw = mulberry32(seed);
21287
- const rng = {
21288
- next: raw,
21289
- nextInt(min, max) {
21290
- return Math.floor(raw() * (max - min + 1)) + min;
21291
- },
21292
- nextFloat(min, max) {
21293
- return raw() * (max - min) + min;
21294
- },
21295
- pick(arr) {
21296
- if (arr.length === 0) {
21297
- throw new Error("Cannot pick from an empty array");
21298
- }
21299
- return arr[Math.floor(raw() * arr.length)];
21300
- },
21301
- pickN(arr, n) {
21302
- const copy = [...arr];
21303
- rng.shuffle(copy);
21304
- return copy.slice(0, Math.min(n, copy.length));
21305
- },
21306
- shuffle(arr) {
21307
- for (let i = arr.length - 1; i > 0; i--) {
21308
- const j = Math.floor(raw() * (i + 1));
21309
- const current = arr[i];
21310
- arr[i] = arr[j];
21311
- arr[j] = current;
21312
- }
21313
- return arr;
21314
- },
21315
- chance(probability) {
21316
- return raw() < probability;
21317
- },
21318
- uuid() {
21319
- const bytes = Array.from({ length: 16 }, () => Math.floor(raw() * 256));
21320
- bytes[6] = bytes[6] & 15 | 64;
21321
- bytes[8] = bytes[8] & 63 | 128;
21322
- const hex = bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
21323
- return [
21324
- hex.slice(0, 8),
21325
- hex.slice(8, 12),
21326
- hex.slice(12, 16),
21327
- hex.slice(16, 20),
21328
- hex.slice(20, 32)
21329
- ].join("-");
21330
- },
21331
- date(start, end) {
21332
- const s = start.getTime();
21333
- const e = end.getTime();
21334
- return new Date(s + raw() * (e - s));
21335
- },
21336
- weightedPick(items, weights) {
21337
- if (items.length === 0) {
21338
- throw new Error("Cannot pick from an empty weighted item list");
21339
- }
21340
- const total = weights.reduce((sum, w) => sum + w, 0);
21341
- let r = raw() * total;
21342
- for (let i = 0; i < items.length; i++) {
21343
- r -= weights[i] ?? 0;
21344
- if (r <= 0) return items[i];
21345
- }
21346
- return items[items.length - 1];
21347
- }
21348
- };
21349
- return rng;
21325
+ // src/pipeline/csv-mappings.ts
21326
+ function getDefaultMappings(sourceSystem, entityType) {
21327
+ return DEFAULT_MAPPINGS[sourceSystem]?.[entityType] ?? {};
21350
21328
  }
21351
- var init_seed = __esm({
21352
- "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"() {
21353
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
+ };
21354
21370
  }
21355
21371
  });
21356
21372
 
21357
- // src/demo/scenarios.ts
21358
- var scenarios_exports = {};
21359
- __export(scenarios_exports, {
21360
- NAMED_DEMO_SCENARIOS: () => NAMED_DEMO_SCENARIOS,
21361
- SCENARIOS: () => SCENARIOS,
21362
- SCENARIO_LIST: () => SCENARIO_LIST,
21363
- blendScenarios: () => blendScenarios,
21364
- getScenario: () => getScenario,
21365
- isNamedDemoScenario: () => isNamedDemoScenario,
21366
- pickRandomScenario: () => pickRandomScenario,
21367
- resolveScenarioInput: () => resolveScenarioInput
21373
+ // src/pipeline/csv-detect.ts
21374
+ var csv_detect_exports = {};
21375
+ __export(csv_detect_exports, {
21376
+ detectEntityType: () => detectEntityType
21368
21377
  });
21369
- function getScenario(key) {
21370
- const scenario = SCENARIOS[key];
21371
- if (!scenario) {
21372
- throw new Error(`Unknown scenario: ${key}. Valid: ${Object.keys(SCENARIOS).join(", ")}`);
21373
- }
21374
- return scenario;
21375
- }
21376
- function isNamedDemoScenario(raw) {
21377
- return NAMED_DEMO_SCENARIOS.includes(raw);
21378
- }
21379
- function resolveScenarioInput(raw) {
21380
- const input = raw?.trim();
21381
- if (!input) return void 0;
21382
- if (input === "research_blend") return "research_blend";
21383
- if (NAMED_DEMO_SCENARIOS.includes(input)) {
21384
- 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
+ }
21385
21393
  }
21386
- const n = Number(input);
21387
- if (Number.isInteger(n) && n >= 1 && n <= NAMED_DEMO_SCENARIOS.length) {
21388
- 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
+ }
21389
21401
  }
21390
- return null;
21391
- }
21392
- function pickRandomScenario() {
21393
- return RANDOM_POOL[Math.floor(Math.random() * RANDOM_POOL.length)];
21402
+ return {
21403
+ entityType: bestMatch.rule.entityType,
21404
+ confidence: Math.round(bestMatch.score * 100) / 100,
21405
+ mappings
21406
+ };
21394
21407
  }
21395
- function blendScenarios(_research) {
21396
- const blended = {
21397
- ...BASELINE,
21398
- key: "research_blend",
21399
- label: "Research-Derived Blend",
21400
- description: "Realistic data with mild-to-moderate problems across all vital signs.",
21401
- story: "Generated with default research blend. Problems are seeded across all vital signs at realistic levels.",
21402
- hook: "Mild-to-moderate problems seeded across all five vitals.",
21403
- // Bump all problems slightly above baseline for discoverability
21404
- staleContactRatio: 0.2,
21405
- pastCloseDateRatio: 0.15,
21406
- mqlDropRatio: 0.15,
21407
- qualifiedNoOutreachRatio: 0.15,
21408
- stuckDealRatio: 0.15,
21409
- noiseActivityRatio: 0.2,
21410
- singleThreadRatio: 0.25
21411
- };
21412
- return blended;
21413
- }
21414
- var BASELINE, SCENARIOS, SCENARIO_LIST, NAMED_DEMO_SCENARIOS, RANDOM_POOL;
21415
- var init_scenarios = __esm({
21416
- "src/demo/scenarios.ts"() {
21408
+ var SIGNATURES;
21409
+ var init_csv_detect = __esm({
21410
+ "src/pipeline/csv-detect.ts"() {
21417
21411
  "use strict";
21418
- BASELINE = {
21419
- enterpriseRatio: 0.2,
21420
- midMarketRatio: 0.3,
21421
- smbRatio: 0.5,
21422
- staleContactRatio: 0.15,
21423
- staleContactRatioEnterprise: 0.2,
21424
- staleContactRatioSmb: 0.1,
21425
- pastCloseDateRatio: 0.1,
21426
- staleDays: 120,
21427
- mqlDropRatio: 0.1,
21428
- qualifiedNoOutreachRatio: 0.1,
21429
- stuckDealRatio: 0.1,
21430
- stuckInNegotiationDays: 45,
21431
- avgDaysPerStageEnterprise: 25,
21432
- avgDaysPerStageSmb: 8,
21433
- activityVolumeMultiplier: 1,
21434
- noiseActivityRatio: 0.15,
21435
- singleThreadRatio: 0.2,
21436
- loneWolfRepIndex: null,
21437
- loneWolfSingleThreadRatio: 0,
21438
- 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
+ ]
21439
21429
  };
21440
- SCENARIOS = {
21441
- hidden_crisis: {
21442
- ...BASELINE,
21443
- key: "hidden_crisis",
21444
- label: "The Hidden Crisis",
21445
- description: "Overall health looks yellow but Enterprise is deep red, masked by strong SMB numbers.",
21446
- 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.",
21447
- hook: "SMB is carrying the average while Enterprise dies quietly.",
21448
- staleContactRatio: 0.3,
21449
- staleContactRatioEnterprise: 0.6,
21450
- staleContactRatioSmb: 0.1,
21451
- singleThreadRatio: 0.5,
21452
- enterpriseRatio: 0.3,
21453
- midMarketRatio: 0.3,
21454
- smbRatio: 0.4
21455
- },
21456
- leaky_bucket: {
21457
- ...BASELINE,
21458
- key: "leaky_bucket",
21459
- label: "The Leaky Bucket",
21460
- description: "Marketing generates plenty of leads but 40% vanish at handoff to sales.",
21461
- 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.",
21462
- hook: "MQLs flow in, then 40% vanish at the sales handoff.",
21463
- mqlDropRatio: 0.4,
21464
- qualifiedNoOutreachRatio: 0.35,
21465
- staleContactRatio: 0.2
21466
- },
21467
- stale_pipeline: {
21468
- ...BASELINE,
21469
- key: "stale_pipeline",
21470
- label: "The Stale Pipeline",
21471
- description: "Big pipeline number but half the deals are zombies stuck in late stages.",
21472
- 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.",
21473
- hook: "Half the pipeline is zombies \u2014 you're forecasting on fiction.",
21474
- pastCloseDateRatio: 0.5,
21475
- stuckDealRatio: 0.4,
21476
- stuckInNegotiationDays: 120,
21477
- staleContactRatio: 0.25,
21478
- staleDays: 90
21479
- },
21480
- lone_wolf: {
21481
- ...BASELINE,
21482
- key: "lone_wolf",
21483
- label: "The Lone Wolf",
21484
- description: "One rep has great numbers but every single deal is single-threaded.",
21485
- 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.",
21486
- hook: "Top rep, huge pipeline, one contact per deal \u2014 one exit from collapse.",
21487
- loneWolfRepIndex: 0,
21488
- loneWolfSingleThreadRatio: 1,
21489
- singleThreadRatio: 0.15
21490
- },
21491
- busy_bees: {
21492
- ...BASELINE,
21493
- key: "busy_bees",
21494
- label: "The Busy Bees",
21495
- description: "High activity volume across the team, but most of it hits dead ends.",
21496
- 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.",
21497
- hook: "Reps are spraying, not aiming.",
21498
- activityVolumeMultiplier: 3,
21499
- noiseActivityRatio: 0.6,
21500
- staleContactRatio: 0.2
21501
- },
21502
- even_keel: {
21503
- ...BASELINE,
21504
- key: "even_keel",
21505
- label: "The Even Keel",
21506
- description: "A reasonably healthy book \u2014 enough yellow to listen, not a five-alarm fire.",
21507
- 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.",
21508
- hook: "Reasonably healthy \u2014 enough signal to listen, not a crisis."
21509
- },
21510
- compound_pain: {
21511
- ...BASELINE,
21512
- key: "compound_pain",
21513
- label: "The Compound Fracture",
21514
- description: "Several vitals are red at once \u2014 stale pipeline, leaky handoff, noisy activity, thin threads.",
21515
- 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.",
21516
- hook: "Several vitals red at once \u2014 the stethoscope has to pick a first listen.",
21517
- enterpriseRatio: 0.3,
21518
- midMarketRatio: 0.3,
21519
- smbRatio: 0.4,
21520
- staleContactRatio: 0.35,
21521
- staleContactRatioEnterprise: 0.55,
21522
- staleContactRatioSmb: 0.15,
21523
- pastCloseDateRatio: 0.35,
21524
- staleDays: 100,
21525
- mqlDropRatio: 0.3,
21526
- qualifiedNoOutreachRatio: 0.25,
21527
- stuckDealRatio: 0.3,
21528
- stuckInNegotiationDays: 90,
21529
- activityVolumeMultiplier: 2,
21530
- noiseActivityRatio: 0.4,
21531
- 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]);
21532
21729
  }
21533
- };
21534
- SCENARIO_LIST = Object.values(SCENARIOS);
21535
- NAMED_DEMO_SCENARIOS = [
21536
- "hidden_crisis",
21537
- "leaky_bucket",
21538
- "stale_pipeline",
21539
- "lone_wolf",
21540
- "busy_bees",
21541
- "even_keel",
21542
- "compound_pain"
21543
- ];
21544
- 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";
21545
21824
  }
21546
21825
  });
21547
21826
 
@@ -23528,320 +23807,93 @@ async function insertDirect(dataset) {
23528
23807
  metadata: opp.metadata,
23529
23808
  created_at: createdAt
23530
23809
  });
23531
- oppIdMap.set(opp.localId, dbId);
23532
- }
23533
- for (const act of dataset.activities) {
23534
- await insertActivity({
23535
- activity_type: act.activity_type,
23536
- occurred_at: act.occurred_at,
23537
- person_id: act.personLocalId ? personIdMap.get(act.personLocalId) ?? null : null,
23538
- organization_id: act.organizationLocalId ? orgIdMap.get(act.organizationLocalId) ?? null : null,
23539
- opportunity_id: act.opportunityLocalId ? oppIdMap.get(act.opportunityLocalId) ?? null : null,
23540
- source_system: act.source_system,
23541
- source_id: act.source_id,
23542
- raw_data: act.raw_data,
23543
- metadata: act.metadata
23544
- });
23545
- }
23546
- for (const campaign of dataset.campaigns) {
23547
- await insertCampaign({
23548
- canonical_name: campaign.canonical_name,
23549
- campaign_type: campaign.campaign_type,
23550
- source_system: campaign.source_system,
23551
- source_id: campaign.source_id,
23552
- raw_data: campaign.raw_data,
23553
- metadata: campaign.metadata
23554
- });
23555
- }
23556
- for (const event of dataset.revenueEvents ?? []) {
23557
- await insertRevenueEvent({
23558
- organization_id: orgIdMap.get(event.organizationLocalId) ?? null,
23559
- period: event.period,
23560
- amount: event.amount,
23561
- event_type: event.event_type,
23562
- source_system: event.source_system,
23563
- source_id: event.source_id,
23564
- raw_data: event.raw_data
23565
- });
23566
- }
23567
- const { invalidateLexiconSeed: invalidateLexiconSeed2 } = await Promise.resolve().then(() => (init_lexicon_seed(), lexicon_seed_exports));
23568
- invalidateLexiconSeed2();
23569
- return {
23570
- mode: "direct",
23571
- counts: {
23572
- organizations: dataset.organizations.length,
23573
- people: dataset.people.length,
23574
- opportunities: dataset.opportunities.length,
23575
- activities: dataset.activities.length,
23576
- campaigns: dataset.campaigns.length,
23577
- revenue_events: dataset.revenueEvents?.length ?? 0
23578
- },
23579
- health: null
23580
- };
23581
- }
23582
- function hashString(str2) {
23583
- let hash = 0;
23584
- for (let i = 0; i < str2.length; i++) {
23585
- const char = str2.charCodeAt(i);
23586
- hash = (hash << 5) - hash + char | 0;
23587
- }
23588
- return Math.abs(hash);
23589
- }
23590
- var init_generator = __esm({
23591
- "src/demo/generator.ts"() {
23592
- "use strict";
23593
- init_queries();
23594
- init_seed();
23595
- init_scenarios();
23596
- init_organizations();
23597
- init_people();
23598
- init_opportunities();
23599
- init_activities();
23600
- init_saas_enrichment();
23601
- init_salesforce();
23602
- init_hubspot();
23603
- init_outreach();
23604
- }
23605
- });
23606
-
23607
- // src/demo/scenario-fit.ts
23608
- function dealBandFromCycleDays(days) {
23609
- if (days == null || !Number.isFinite(days) || days <= 0) return void 0;
23610
- if (days <= 21) return "velocity";
23611
- if (days <= 45) return "core";
23612
- if (days <= 90) return "mid";
23613
- return "enterprise";
23614
- }
23615
- function dealBandFromAverageDealSize(raw) {
23616
- if (!raw) return void 0;
23617
- const t = raw.trim().toLowerCase();
23618
- if (!t) return void 0;
23619
- const match = t.match(/(\d+(?:\.\d+)?)\s*(k|m|million|thousand)?/i);
23620
- if (!match) return void 0;
23621
- let n = Number(match[1]);
23622
- if (!Number.isFinite(n)) return void 0;
23623
- const unit = (match[2] ?? "").toLowerCase();
23624
- if (unit === "k" || unit === "thousand") n *= 1e3;
23625
- else if (unit === "m" || unit === "million") n *= 1e6;
23626
- else if (n > 0 && n < 500) n *= 1e3;
23627
- if (n < 15e3) return "velocity";
23628
- if (n < 5e4) return "core";
23629
- if (n < 15e4) return "mid";
23630
- return "enterprise";
23631
- }
23632
- function signalsFromProfile(profile) {
23633
- if (!profile) return {};
23634
- const text = [
23635
- profile.industry,
23636
- profile.product_description,
23637
- profile.target_customer,
23638
- profile.user_scope,
23639
- profile.custom_context
23640
- ].filter(Boolean).join("\n");
23641
- return {
23642
- salesMotion: profile.sales_motion,
23643
- dealBand: dealBandFromAverageDealSize(profile.average_deal_size) ?? dealBandFromCycleDays(profile.sales_cycle_days),
23644
- cycleDays: profile.sales_cycle_days,
23645
- text
23646
- };
23647
- }
23648
- function keywordHits(text) {
23649
- const hits = {};
23650
- const hay = text.toLowerCase();
23651
- for (const { scenario, patterns } of KEYWORD_HINTS) {
23652
- let n = 0;
23653
- for (const re of patterns) {
23654
- if (re.test(hay)) n++;
23655
- }
23656
- if (n > 0) hits[scenario] = n;
23657
- }
23658
- return hits;
23659
- }
23660
- function matrixPick(motion, band) {
23661
- if (motion && band) return MATRIX[motion][band];
23662
- if (motion) return MOTION_ONLY[motion];
23663
- if (band) return BAND_ONLY[band];
23664
- return "even_keel";
23665
- }
23666
- function reasonFor(scenario, signals, via) {
23667
- const s = getScenario(scenario);
23668
- if (via === "keywords") {
23669
- return `Your notes sound like ${s.label} \u2014 ${s.hook}`;
23810
+ oppIdMap.set(opp.localId, dbId);
23670
23811
  }
23671
- const motion = signals.salesMotion;
23672
- const band = signals.dealBand;
23673
- if (motion && band) {
23674
- 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
+ });
23675
23824
  }
23676
- if (motion) return `${labelMotion(motion)} books usually show up as ${s.label}.`;
23677
- if (band) return `${labelBand(band)} deals usually show up as ${s.label}.`;
23678
- return `${s.label} is the even-keeled starting book when we don't know the motion yet.`;
23679
- }
23680
- function labelMotion(m) {
23681
- return MOTION_FIT_CHOICES.find((c) => c.value === m)?.label ?? m;
23682
- }
23683
- function labelBand(b) {
23684
- return DEAL_BAND_CHOICES.find((c) => c.value === b)?.label ?? b;
23685
- }
23686
- function inferDemoScenario(signals) {
23687
- const text = signals.text?.trim() ?? "";
23688
- if (text) {
23689
- const hits = keywordHits(text);
23690
- let best;
23691
- let bestN = 0;
23692
- for (const id of NAMED_DEMO_SCENARIOS) {
23693
- const n = hits[id] ?? 0;
23694
- if (n > bestN) {
23695
- best = id;
23696
- bestN = n;
23697
- }
23698
- }
23699
- if (best && bestN > 0) {
23700
- return { scenario: best, reason: reasonFor(best, signals, "keywords"), source: "heuristic" };
23701
- }
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
+ });
23702
23834
  }
23703
- const scenario = matrixPick(signals.salesMotion, signals.dealBand);
23704
- return { scenario, reason: reasonFor(scenario, signals, "matrix"), source: "heuristic" };
23705
- }
23706
- function getPreferredDemoScenario() {
23707
- const raw = getConfigValue(PREF_SCENARIO_KEY);
23708
- return raw && isNamedDemoScenario(raw) ? raw : void 0;
23709
- }
23710
- function saveDemoFit(opts) {
23711
- setConfigValue(PREF_SCENARIO_KEY, opts.scenario);
23712
- if (opts.motion) {
23713
- setConfigValue(PREF_MOTION_KEY, opts.motion);
23714
- if (opts.syncSalesMotion && !isProfileConfigured(loadProfile())) {
23715
- setConfigValue("sales-motion", opts.motion);
23716
- }
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
+ });
23717
23845
  }
23718
- 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
+ };
23719
23860
  }
23720
- function scenarioMenuChoices() {
23721
- return NAMED_DEMO_SCENARIOS.map((id) => {
23722
- const s = getScenario(id);
23723
- return {
23724
- value: id,
23725
- label: s.label,
23726
- description: s.hook
23727
- };
23728
- });
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);
23729
23868
  }
23730
- var PREF_SCENARIO_KEY, PREF_MOTION_KEY, PREF_BAND_KEY, MOTION_FIT_CHOICES, DEAL_BAND_CHOICES, MATRIX, MOTION_ONLY, BAND_ONLY, KEYWORD_HINTS;
23731
- var init_scenario_fit = __esm({
23732
- "src/demo/scenario-fit.ts"() {
23869
+ var init_generator = __esm({
23870
+ "src/demo/generator.ts"() {
23733
23871
  "use strict";
23734
- init_store();
23735
- init_profile();
23872
+ init_queries();
23873
+ init_seed();
23736
23874
  init_scenarios();
23737
- PREF_SCENARIO_KEY = "demo-scenario-preference";
23738
- PREF_MOTION_KEY = "demo-fit-motion";
23739
- PREF_BAND_KEY = "demo-fit-deal-band";
23740
- MOTION_FIT_CHOICES = [
23741
- {
23742
- value: "plg",
23743
- label: "Product-led / self-serve",
23744
- description: "Users start themselves; sales assists or expands"
23745
- },
23746
- {
23747
- value: "smb_velocity",
23748
- label: "High-volume SMB",
23749
- description: "Fast cycles, lots of small deals, outbound or inbound machine"
23750
- },
23751
- {
23752
- value: "mid_market",
23753
- label: "Mid-market, structured process",
23754
- description: "A real sales cycle, a few stakeholders, moderate ACV"
23755
- },
23756
- {
23757
- value: "enterprise",
23758
- label: "Enterprise, long cycles",
23759
- description: "Large deals, many buyers, quarters not weeks"
23760
- }
23761
- ];
23762
- DEAL_BAND_CHOICES = [
23763
- {
23764
- value: "velocity",
23765
- label: "Under ~$15K, days to a couple of weeks",
23766
- description: "Velocity / transactional"
23767
- },
23768
- {
23769
- value: "core",
23770
- label: "~$15K\u2013$50K, a few weeks",
23771
- description: "Core SMB"
23772
- },
23773
- {
23774
- value: "mid",
23775
- label: "~$50K\u2013$150K, 1\u20133 months",
23776
- description: "Classic mid-market"
23777
- },
23778
- {
23779
- value: "enterprise",
23780
- label: "$150K+, a quarter or more",
23781
- description: "Enterprise / strategic"
23782
- }
23783
- ];
23784
- MATRIX = {
23785
- plg: {
23786
- velocity: "leaky_bucket",
23787
- core: "leaky_bucket",
23788
- mid: "hidden_crisis",
23789
- enterprise: "hidden_crisis"
23790
- },
23791
- smb_velocity: {
23792
- velocity: "busy_bees",
23793
- core: "busy_bees",
23794
- mid: "leaky_bucket",
23795
- enterprise: "lone_wolf"
23796
- },
23797
- mid_market: {
23798
- velocity: "busy_bees",
23799
- core: "stale_pipeline",
23800
- mid: "stale_pipeline",
23801
- enterprise: "hidden_crisis"
23802
- },
23803
- enterprise: {
23804
- velocity: "lone_wolf",
23805
- core: "lone_wolf",
23806
- mid: "hidden_crisis",
23807
- enterprise: "hidden_crisis"
23808
- }
23809
- };
23810
- MOTION_ONLY = {
23811
- plg: "leaky_bucket",
23812
- smb_velocity: "busy_bees",
23813
- mid_market: "stale_pipeline",
23814
- enterprise: "hidden_crisis"
23815
- };
23816
- BAND_ONLY = {
23817
- velocity: "busy_bees",
23818
- core: "leaky_bucket",
23819
- mid: "stale_pipeline",
23820
- enterprise: "hidden_crisis"
23821
- };
23822
- KEYWORD_HINTS = [
23823
- { scenario: "even_keel", patterns: [/\beven keel\b/, /\breasonably healthy\b/, /\bno crisis\b/, /\bjust evaluating\b/, /\bgreen[- ]field\b/] },
23824
- { scenario: "compound_pain", patterns: [/\beverything('?s| is) (on fire|red|broken)\b/, /\bcompound\b/, /\ball (five )?vitals\b/, /\bmultiple problems\b/] },
23825
- { scenario: "leaky_bucket", patterns: [/\bhandoff\b/, /\bmqls?\b/, /\bleak/, /\bdrop[- ]rate/, /\bvanish/, /\brouting\b/, /\bmarketing.?sales\b/] },
23826
- { scenario: "stale_pipeline", patterns: [/\bzombie/, /\bstale\b/, /\bpast[- ]due\b/, /\bforecast(ing)? on fiction\b/, /\bstuck in negotiation\b/, /\bquiet deals?\b/] },
23827
- { scenario: "lone_wolf", patterns: [/\bsingle[- ]thread/, /\blone wolf\b/, /\bone contact\b/, /\bchampion leaves\b/] },
23828
- { scenario: "busy_bees", patterns: [/\bspray/, /\bnois(e|y)\b/, /\bmisdirected\b/, /\bactivity (volume|metrics)\b/, /\bbusy bees\b/, /\bnot (on|hitting) pipeline\b/] },
23829
- { scenario: "hidden_crisis", patterns: [/\bhidden crisis\b/, /\benterprise (is )?(dying|red|stale)\b/, /\bsegment.{0,20}mask/, /\baverages? (look|looks) (fine|okay|yellow)\b/] }
23830
- ];
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();
23831
23883
  }
23832
23884
  });
23833
23885
 
23834
23886
  // src/demo/taxonomy-cache.ts
23835
- 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";
23836
23888
  import { homedir as homedir7 } from "os";
23837
23889
  import { join as join24 } from "path";
23838
23890
  function ensureDir6() {
23839
- if (!existsSync22(NTRP_DIR4)) {
23891
+ if (!existsSync23(NTRP_DIR4)) {
23840
23892
  mkdirSync13(NTRP_DIR4, { recursive: true });
23841
23893
  }
23842
23894
  }
23843
23895
  function loadCachedTaxonomy(profile) {
23844
- if (!existsSync22(TAXONOMY_PATH)) return null;
23896
+ if (!existsSync23(TAXONOMY_PATH)) return null;
23845
23897
  try {
23846
23898
  const parsed = JSON.parse(readFileSync19(TAXONOMY_PATH, "utf-8"));
23847
23899
  if (!parsed || typeof parsed !== "object") return null;
@@ -24318,7 +24370,7 @@ __export(inbox_setup_exports, {
24318
24370
  shouldOfferInboxSkillSetup: () => shouldOfferInboxSkillSetup
24319
24371
  });
24320
24372
  import chalk27 from "chalk";
24321
- import { existsSync as existsSync23 } from "fs";
24373
+ import { existsSync as existsSync24 } from "fs";
24322
24374
  function markDemoOffered() {
24323
24375
  setConfigValue("ai-inbox-nudge-seen", "true");
24324
24376
  }
@@ -24350,7 +24402,7 @@ function printSkipHint(beat) {
24350
24402
  async function reuseInboxFolderIfPresent(session, beat, folderPath) {
24351
24403
  if (getAiInboxDir()) return false;
24352
24404
  const candidates = folderPath ? [folderPath] : [.../* @__PURE__ */ new Set([defaultAiInboxDir(), legacyAiInboxDir()])];
24353
- const existing = candidates.find((p) => existsSync23(p));
24405
+ const existing = candidates.find((p) => existsSync24(p));
24354
24406
  if (!existing) return false;
24355
24407
  console.log(" " + chalk27.dim("Pickup folder still on disk: ") + existing);
24356
24408
  const reuse = await session.confirm("Reuse this pickup folder?", true);
@@ -24456,7 +24508,7 @@ __export(ingest_exports, {
24456
24508
  handler: () => handler3
24457
24509
  });
24458
24510
  import chalk28 from "chalk";
24459
- import { readFileSync as readFileSync20, existsSync as existsSync24 } from "fs";
24511
+ import { readFileSync as readFileSync20, existsSync as existsSync25 } from "fs";
24460
24512
  import { basename as basename6 } from "path";
24461
24513
  async function handler3(args, ctx) {
24462
24514
  const { positional, flags } = parseArgs(args, [
@@ -24480,7 +24532,7 @@ async function handler3(args, ctx) {
24480
24532
  console.error(chalk28.dim(" /ingest --demo [--scenario <name>]"));
24481
24533
  process.exit(1);
24482
24534
  }
24483
- if (!existsSync24(file)) {
24535
+ if (!existsSync25(file)) {
24484
24536
  console.error(chalk28.red(` File not found: ${file}`));
24485
24537
  process.exit(1);
24486
24538
  }
@@ -24862,39 +24914,54 @@ var ingest_chat_exports = {};
24862
24914
  __export(ingest_chat_exports, {
24863
24915
  extractFilePath: () => extractFilePath,
24864
24916
  ingestFromChat: () => ingestFromChat,
24917
+ ingestPathFromChat: () => ingestPathFromChat,
24865
24918
  isDemoIntent: () => isDemoIntent,
24919
+ listCsvsInFolder: () => listCsvsInFolder,
24866
24920
  loadDemoFromChat: () => loadDemoFromChat,
24867
24921
  looksLikeFilePath: () => looksLikeFilePath
24868
24922
  });
24869
- import { existsSync as existsSync25 } from "fs";
24870
- 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";
24871
24925
  import { homedir as homedir8 } from "os";
24872
24926
  import chalk30 from "chalk";
24873
24927
  function extractFilePath(input) {
24874
- const trimmed = input.trim();
24928
+ const trimmed = input.trim().replace(/^["']|["']$/g, "");
24929
+ if (!trimmed) return null;
24875
24930
  const patterns = [
24876
- /^["'](.+\.csv)["']$/i,
24877
- /^@(.+\.csv)$/i,
24878
- /(?:here'?s|file|path|upload)[:\s]+["']?([^\s"']+\.csv)["']?/i,
24879
- /^~\/\S+\.csv$/i,
24880
- /^\.\.?\/\S+\.csv$/i,
24881
- /^\/\S+\.csv$/i,
24882
- /^[A-Za-z]:\\[^\s]+\.csv$/i,
24883
- /^[^\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
24884
24939
  ];
24885
24940
  for (const re of patterns) {
24886
24941
  const m = trimmed.match(re);
24887
- if (m?.[1]) {
24888
- const p = expandPath(m[1]);
24889
- if (existsSync25(p)) return p;
24890
- }
24891
- if (!m?.[1] && re.test(trimmed) && trimmed.toLowerCase().endsWith(".csv")) {
24892
- const p = expandPath(trimmed.replace(/^["']|["']$/g, ""));
24893
- 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
+ }
24894
24952
  }
24895
24953
  }
24896
24954
  return null;
24897
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
+ }
24898
24965
  function expandPath(p) {
24899
24966
  if (p.startsWith("~/")) return resolve9(homedir8(), p.slice(2));
24900
24967
  return resolve9(p);
@@ -24902,6 +24969,73 @@ function expandPath(p) {
24902
24969
  function looksLikeFilePath(input) {
24903
24970
  return extractFilePath(input) !== null;
24904
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
+ }
24905
25039
  async function ingestFromChat(ctx, filePath) {
24906
25040
  if (!ctx.rl) {
24907
25041
  console.log(" " + chalk30.red("Ingest confirm requires interactive mode."));
@@ -24966,6 +25100,7 @@ async function ingestFromChat(ctx, filePath) {
24966
25100
  };
24967
25101
  invalidateGapAudit(ctx);
24968
25102
  saveSessionState(ctx);
25103
+ markProductionDataSeen();
24969
25104
  console.log();
24970
25105
  console.log(" " + paint("accent", "\u2713 Data loaded") + chalk30.dim(` \u2014 ${name}`));
24971
25106
  recordMessage(ctx, "user", `[ingested ${name}]`);
@@ -25041,6 +25176,7 @@ async function loadDemoFromChat(ctx, scenario, opts = {}) {
25041
25176
  counts,
25042
25177
  ingested_at: (/* @__PURE__ */ new Date()).toISOString()
25043
25178
  };
25179
+ markDemoDataSeen();
25044
25180
  if (!ctx.scope) {
25045
25181
  const { proposeScopeFromIntent: proposeScopeFromIntent2 } = await Promise.resolve().then(() => (init_scope(), scope_exports));
25046
25182
  const proposal = proposeScopeFromIntent2("demo pipeline and metrics");
@@ -25077,6 +25213,7 @@ var init_ingest_chat = __esm({
25077
25213
  init_compute2();
25078
25214
  init_theme();
25079
25215
  init_demo();
25216
+ init_onboard_tiers();
25080
25217
  }
25081
25218
  });
25082
25219
 
@@ -25835,9 +25972,9 @@ async function handleGetSessionBrief(input) {
25835
25972
  if (!target) {
25836
25973
  return { error: `No session matching "${raw}".` };
25837
25974
  }
25838
- const { existsSync: existsSync28, readFileSync: readFileSync23 } = await import("fs");
25975
+ const { existsSync: existsSync29, readFileSync: readFileSync23 } = await import("fs");
25839
25976
  const briefPath = contextDocPathForSession2(target.id);
25840
- if (!existsSync28(briefPath)) {
25977
+ if (!existsSync29(briefPath)) {
25841
25978
  return {
25842
25979
  session_id: target.id,
25843
25980
  error: "No context brief on disk for this session (created before brief storage existed).",
@@ -26852,14 +26989,14 @@ var init_ask = __esm({
26852
26989
  });
26853
26990
 
26854
26991
  // src/services/setup.ts
26855
- import { existsSync as existsSync26, mkdirSync as mkdirSync14, readFileSync as readFileSync21, writeFileSync as writeFileSync16 } from "fs";
26856
- 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";
26857
26994
  function setupCheck() {
26858
26995
  const home = ntrpHome();
26859
26996
  let writable = false;
26860
26997
  try {
26861
26998
  mkdirSync14(home, { recursive: true });
26862
- const probe = join25(home, ".write-check");
26999
+ const probe = join26(home, ".write-check");
26863
27000
  writeFileSync16(probe, "ok\n");
26864
27001
  writable = true;
26865
27002
  } catch {
@@ -26911,11 +27048,11 @@ var init_setup = __esm({
26911
27048
  });
26912
27049
 
26913
27050
  // src/version.ts
26914
- import { existsSync as existsSync27, readFileSync as readFileSync22 } from "fs";
26915
- 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";
26916
27053
  import { fileURLToPath } from "url";
26917
27054
  function readVersionFromPackageJson(packageJsonPath) {
26918
- if (!existsSync27(packageJsonPath)) return null;
27055
+ if (!existsSync28(packageJsonPath)) return null;
26919
27056
  try {
26920
27057
  const pkg = JSON.parse(readFileSync22(packageJsonPath, "utf-8"));
26921
27058
  if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
@@ -26925,7 +27062,7 @@ function readVersionFromPackageJson(packageJsonPath) {
26925
27062
  }
26926
27063
  function readVersionNearEntry(entryPath) {
26927
27064
  const start = dirname5(entryPath);
26928
- 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")]) {
26929
27066
  const version = readVersionFromPackageJson(rel);
26930
27067
  if (version) return version;
26931
27068
  }