@sonnechasser/ntrp 1.4.9 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +729 -166
  2. package/dist/mcp/server.js +1116 -993
  3. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -6587,6 +6587,10 @@ function stripTools(req) {
6587
6587
  return rest;
6588
6588
  }
6589
6589
  async function outboundRequest(req) {
6590
+ if (req.skipPseudonymize) {
6591
+ const { skipPseudonymize: _drop, ...rest } = req;
6592
+ return rest;
6593
+ }
6590
6594
  await ensureLexiconSeeded();
6591
6595
  return protectRequest(req);
6592
6596
  }
@@ -6848,13 +6852,14 @@ var init_failover = __esm({
6848
6852
  });
6849
6853
 
6850
6854
  // src/ai/llm/complete.ts
6851
- async function llmCompleteText(surface, system, userMessage, max_tokens, ctx) {
6855
+ async function llmCompleteText(surface, system, userMessage, max_tokens, ctx, opts = {}) {
6852
6856
  const { response, meta } = await completeWithFailover(
6853
6857
  {
6854
6858
  surface,
6855
6859
  system,
6856
6860
  messages: [{ role: "user", content: userMessage }],
6857
- max_tokens
6861
+ max_tokens,
6862
+ ...opts.skipPseudonymize ? { skipPseudonymize: true } : {}
6858
6863
  },
6859
6864
  { ctx }
6860
6865
  );
@@ -22822,9 +22827,9 @@ async function handleGetSessionBrief(input) {
22822
22827
  if (!target) {
22823
22828
  return { error: `No session matching "${raw}".` };
22824
22829
  }
22825
- const { existsSync: existsSync36, readFileSync: readFileSync24 } = await import("fs");
22830
+ const { existsSync: existsSync38, readFileSync: readFileSync24 } = await import("fs");
22826
22831
  const briefPath = contextDocPathForSession2(target.id);
22827
- if (!existsSync36(briefPath)) {
22832
+ if (!existsSync38(briefPath)) {
22828
22833
  return {
22829
22834
  session_id: target.id,
22830
22835
  error: "No context brief on disk for this session (created before brief storage existed).",
@@ -24277,20 +24282,128 @@ var init_diagnose = __esm({
24277
24282
  });
24278
24283
 
24279
24284
  // src/ai/profile-draft.ts
24280
- async function draftCompanyProfile(seed, _ctx) {
24285
+ async function draftCompanyProfile(seed, ctx, opts = {}) {
24281
24286
  if (!seed.company_url && !seed.company_name) {
24282
24287
  throw new Error("draftCompanyProfile requires a URL or a company name");
24283
24288
  }
24284
- seedOperatorIdentity(seed.company_name, seed.company_url);
24289
+ const wantResearch = opts.research === true || opts.research !== false && hasAnyLlmProvider();
24290
+ if (!wantResearch) {
24291
+ seedOperatorIdentity(seed.company_name, seed.company_url);
24292
+ const out = {};
24293
+ if (seed.company_name?.trim()) out.company_name = seed.company_name.trim();
24294
+ if (seed.company_url) out.company_url = seed.company_url;
24295
+ return out;
24296
+ }
24297
+ if (!ctx) {
24298
+ throw new Error("draftCompanyProfile research requires a Context");
24299
+ }
24300
+ assertReplAi(ctx);
24301
+ const inputBlock = seed.company_url ? `Company website: ${seed.company_url}${seed.company_name ? `
24302
+ Company name (hint): ${seed.company_name}` : ""}` : `Company name: ${seed.company_name}`;
24303
+ const userMessage = `${inputBlock}
24304
+
24305
+ Research this company deeply using your training knowledge. Identify the canonical company name, what they sell, who buys it, and how they sell it. Emit the profile now as STRICT JSON.`;
24306
+ const { text } = await llmCompleteText("onboard", SYSTEM_PROMPT4, userMessage, 2048, ctx, {
24307
+ skipPseudonymize: true
24308
+ });
24309
+ const cleaned = stripFences3(text);
24310
+ let parsed;
24311
+ try {
24312
+ parsed = JSON.parse(cleaned);
24313
+ } catch {
24314
+ throw new Error("AI response is not valid JSON");
24315
+ }
24316
+ const draft = validateDraft(parsed);
24317
+ const nameForLexicon = draft.company_name ?? seed.company_name;
24318
+ seedOperatorIdentity(nameForLexicon, seed.company_url);
24319
+ if (seed.company_url && !draft.company_url) draft.company_url = seed.company_url;
24320
+ return draft;
24321
+ }
24322
+ function stripFences3(text) {
24323
+ const trimmed = text.trim();
24324
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
24325
+ if (fenced) return fenced[1].trim();
24326
+ return trimmed;
24327
+ }
24328
+ function validateDraft(raw) {
24285
24329
  const out = {};
24286
- if (seed.company_name?.trim()) out.company_name = seed.company_name.trim();
24287
- if (seed.company_url) out.company_url = seed.company_url;
24330
+ const str2 = (k) => {
24331
+ const v = raw[k];
24332
+ return typeof v === "string" && v.trim().length > 0 ? v.trim() : void 0;
24333
+ };
24334
+ const companyName = str2("company_name");
24335
+ if (companyName) out.company_name = companyName;
24336
+ const industry = str2("industry");
24337
+ if (industry) out.industry = industry;
24338
+ const productDescription = str2("product_description");
24339
+ if (productDescription) out.product_description = productDescription;
24340
+ const targetCustomer = str2("target_customer");
24341
+ if (targetCustomer) out.target_customer = targetCustomer;
24342
+ const motion = str2("sales_motion")?.toLowerCase();
24343
+ if (motion && ALLOWED_MOTIONS.has(motion)) {
24344
+ out.sales_motion = motion;
24345
+ }
24346
+ const dealSize = str2("average_deal_size");
24347
+ if (dealSize) out.average_deal_size = dealSize;
24348
+ const cycleDays = raw["sales_cycle_days"];
24349
+ if (typeof cycleDays === "number" && Number.isFinite(cycleDays) && cycleDays > 0) {
24350
+ out.sales_cycle_days = Math.round(cycleDays);
24351
+ }
24352
+ const crm = str2("primary_crm")?.toLowerCase();
24353
+ if (crm && ALLOWED_CRMS.has(crm)) out.primary_crm = crm;
24354
+ const engagement = str2("engagement_tool")?.toLowerCase();
24355
+ if (engagement && ALLOWED_ENGAGEMENT.has(engagement)) out.engagement_tool = engagement;
24288
24356
  return out;
24289
24357
  }
24358
+ var SYSTEM_PROMPT4, ALLOWED_MOTIONS, ALLOWED_CRMS, ALLOWED_ENGAGEMENT;
24290
24359
  var init_profile_draft = __esm({
24291
24360
  "src/ai/profile-draft.ts"() {
24292
24361
  "use strict";
24362
+ init_repl_api();
24363
+ init_complete();
24293
24364
  init_lexicon_seed();
24365
+ SYSTEM_PROMPT4 = `You are a senior GTM researcher with deep recall of the B2B / SaaS / services landscape. Given a company website (or, as a fallback, a plain company name), you produce a rich, specific profile of the business so a pipeline-health tool can tailor every answer to their reality.
24366
+
24367
+ RESEARCH DEPTH \u2014 this is the most important part:
24368
+ - Recognize the company from your training knowledge whenever possible. Pull on everything you know: product line, founding story, funding stage, acquisitions, known customers, pricing tiers, sales process, typical buyer titles, competitive landscape.
24369
+ - When you don't recognize the exact company, infer aggressively but conservatively from the URL, TLD, subdomain, naming conventions, and any industry signals. A domain like "acmedental.com" strongly suggests dental-industry B2B; "quantumledger.io" suggests a fintech/SaaS.
24370
+ - Think about the shape of their GTM: do they sell top-down to CIOs (enterprise), bottom-up to developers (PLG), to SMB owners (velocity), or mid-market (structured)?
24371
+ - Identify the most probable sales motion. Your guess becomes the wizard's default.
24372
+ - For known companies, be specific: reference real product names, real ICP, real buyer personas. For unknown ones, be honest \u2014 use generic-but-plausible descriptors and keep dollar figures as ranges.
24373
+ - NEVER invent specific revenue figures, customer counts, or headcount numbers. Stick to public ranges ("$5K-$50K ACV", "~$250K+ ACV", "mid-market $20M-$200M revenue", etc.).
24374
+
24375
+ FIELDS \u2014 emit only the ones you're reasonably confident about; the wizard will ask the user interactively for anything you omit.
24376
+
24377
+ REQUIRED (emit these whenever possible):
24378
+ - company_name: string \u2014 the canonical marketed name of the company (e.g. "HashiCorp", not "hashicorp.com")
24379
+ - industry: string \u2014 very specific; include both category and sub-category (e.g. "B2B SaaS \u2014 infrastructure automation / developer tools", "B2B services \u2014 dental lab equipment & supplies", "Fintech \u2014 B2B embedded payments")
24380
+ - product_description: string \u2014 2-4 sentences describing what they sell, who pays for it, and how it's delivered (SaaS subscription, hardware + service, self-serve, etc.)
24381
+ - target_customer: string \u2014 plain-English ICP: company size, geography, persona titles, deal triggers. Be specific.
24382
+ - sales_motion: "plg" | "smb_velocity" | "mid_market" | "enterprise" \u2014 the motion most consistent with the product + ICP
24383
+
24384
+ OPTIONAL (include only with reasonable confidence):
24385
+ - average_deal_size: string \u2014 a range like "$5K-$50K ACV", "$45K-$120K (hardware + year 1 software)", "$250K+"
24386
+ - sales_cycle_days: integer \u2014 median cycle length in days
24387
+ - primary_crm: "salesforce" | "hubspot" | "pipedrive" | "other" \u2014 only with a signal (brand tier, pricing, known choice)
24388
+ - engagement_tool: "outreach" | "salesloft" | "apollo" | "none" \u2014 same bar
24389
+
24390
+ OUTPUT FORMAT \u2014 respond with STRICT JSON only, no preamble, no markdown fences, no commentary:
24391
+ {
24392
+ "company_name": "string",
24393
+ "industry": "string",
24394
+ "product_description": "string",
24395
+ "target_customer": "string",
24396
+ "sales_motion": "plg" | "smb_velocity" | "mid_market" | "enterprise",
24397
+ "average_deal_size": "string",
24398
+ "sales_cycle_days": number,
24399
+ "primary_crm": "salesforce" | "hubspot" | "pipedrive" | "other",
24400
+ "engagement_tool": "outreach" | "salesloft" | "apollo" | "none"
24401
+ }
24402
+
24403
+ Any field you are not confident about MUST be omitted entirely (do not include it as null, empty string, or "unknown"). The wizard will fall through to scripted Q&A to fill gaps.`;
24404
+ ALLOWED_MOTIONS = /* @__PURE__ */ new Set(["plg", "smb_velocity", "mid_market", "enterprise"]);
24405
+ ALLOWED_CRMS = /* @__PURE__ */ new Set(["salesforce", "hubspot", "pipedrive", "other"]);
24406
+ ALLOWED_ENGAGEMENT = /* @__PURE__ */ new Set(["outreach", "salesloft", "apollo", "none"]);
24294
24407
  }
24295
24408
  });
24296
24409
 
@@ -24532,7 +24645,7 @@ OUTPUT \u2014 STRICT JSON only, no fences, no commentary:
24532
24645
  });
24533
24646
 
24534
24647
  // src/ai/profile-clarify.ts
24535
- function stripFences3(text) {
24648
+ function stripFences4(text) {
24536
24649
  const trimmed = text.trim();
24537
24650
  const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
24538
24651
  if (fenced) return fenced[1].trim();
@@ -24552,7 +24665,7 @@ function formatDraft(draft) {
24552
24665
  return parts.join("\n");
24553
24666
  }
24554
24667
  function parseClarifyingQuestions(text) {
24555
- const cleaned = stripFences3(text);
24668
+ const cleaned = stripFences4(text);
24556
24669
  let parsed;
24557
24670
  try {
24558
24671
  parsed = JSON.parse(cleaned);
@@ -24649,7 +24762,7 @@ ${draftBlock}${userBlock}${answersBlock}
24649
24762
 
24650
24763
  Emit the refined profile now as STRICT JSON.`;
24651
24764
  const { text } = await llmCompleteText("onboard", REFINE_SYSTEM_PROMPT, userMessage, 2048, ctx);
24652
- const cleaned = stripFences3(text);
24765
+ const cleaned = stripFences4(text);
24653
24766
  let parsed;
24654
24767
  try {
24655
24768
  parsed = JSON.parse(cleaned);
@@ -24671,7 +24784,7 @@ function validateRefined(raw) {
24671
24784
  const targetCustomer = str2("target_customer");
24672
24785
  if (targetCustomer) out.target_customer = targetCustomer;
24673
24786
  const motion = str2("sales_motion")?.toLowerCase();
24674
- if (motion && ALLOWED_MOTIONS.has(motion)) {
24787
+ if (motion && ALLOWED_MOTIONS2.has(motion)) {
24675
24788
  out.sales_motion = motion;
24676
24789
  }
24677
24790
  const dealSize = str2("average_deal_size");
@@ -24681,14 +24794,14 @@ function validateRefined(raw) {
24681
24794
  out.sales_cycle_days = Math.round(cycleDays);
24682
24795
  }
24683
24796
  const crm = str2("primary_crm")?.toLowerCase();
24684
- if (crm && ALLOWED_CRMS.has(crm)) out.primary_crm = crm;
24797
+ if (crm && ALLOWED_CRMS2.has(crm)) out.primary_crm = crm;
24685
24798
  const engagement = str2("engagement_tool")?.toLowerCase();
24686
- if (engagement && ALLOWED_ENGAGEMENT.has(engagement)) out.engagement_tool = engagement;
24799
+ if (engagement && ALLOWED_ENGAGEMENT2.has(engagement)) out.engagement_tool = engagement;
24687
24800
  const userScope = str2("user_scope");
24688
24801
  if (userScope) out.user_scope = userScope;
24689
24802
  return out;
24690
24803
  }
24691
- var CLARIFY_SYSTEM_PROMPT, REFINE_SYSTEM_PROMPT, ALLOWED_MOTIONS, ALLOWED_CRMS, ALLOWED_ENGAGEMENT;
24804
+ var CLARIFY_SYSTEM_PROMPT, REFINE_SYSTEM_PROMPT, ALLOWED_MOTIONS2, ALLOWED_CRMS2, ALLOWED_ENGAGEMENT2;
24692
24805
  var init_profile_clarify = __esm({
24693
24806
  "src/ai/profile-clarify.ts"() {
24694
24807
  "use strict";
@@ -24753,9 +24866,9 @@ OUTPUT FORMAT \u2014 respond with STRICT JSON only, no preamble, no markdown fen
24753
24866
  "engagement_tool": "outreach",
24754
24867
  "user_scope": "string"
24755
24868
  }`;
24756
- ALLOWED_MOTIONS = /* @__PURE__ */ new Set(["plg", "smb_velocity", "mid_market", "enterprise"]);
24757
- ALLOWED_CRMS = /* @__PURE__ */ new Set(["salesforce", "hubspot", "pipedrive", "other"]);
24758
- ALLOWED_ENGAGEMENT = /* @__PURE__ */ new Set(["outreach", "salesloft", "apollo", "none"]);
24869
+ ALLOWED_MOTIONS2 = /* @__PURE__ */ new Set(["plg", "smb_velocity", "mid_market", "enterprise"]);
24870
+ ALLOWED_CRMS2 = /* @__PURE__ */ new Set(["salesforce", "hubspot", "pipedrive", "other"]);
24871
+ ALLOWED_ENGAGEMENT2 = /* @__PURE__ */ new Set(["outreach", "salesloft", "apollo", "none"]);
24759
24872
  }
24760
24873
  });
24761
24874
 
@@ -25014,6 +25127,155 @@ var init_profile2 = __esm({
25014
25127
  }
25015
25128
  });
25016
25129
 
25130
+ // src/conversation/onboard-tiers.ts
25131
+ var onboard_tiers_exports = {};
25132
+ __export(onboard_tiers_exports, {
25133
+ ONBOARD_TIER_ORDER: () => ONBOARD_TIER_ORDER,
25134
+ canRunDomainTier: () => canRunDomainTier,
25135
+ clearOnboardTierFlags: () => clearOnboardTierFlags,
25136
+ describeOnboardTier: () => describeOnboardTier,
25137
+ getOnboardTierFlag: () => getOnboardTierFlag,
25138
+ getOnboardTierStatus: () => getOnboardTierStatus,
25139
+ listCompletedOnboardTier: () => listCompletedOnboardTier,
25140
+ markDemoDataSeen: () => markDemoDataSeen,
25141
+ markOnboardTierComplete: () => markOnboardTierComplete,
25142
+ markProductionDataSeen: () => markProductionDataSeen,
25143
+ pathLooksPresent: () => pathLooksPresent,
25144
+ resetOnboardTierProgress: () => resetOnboardTierProgress,
25145
+ resolveNextOnboardTier: () => resolveNextOnboardTier
25146
+ });
25147
+ import { existsSync as existsSync25, statSync as statSync4 } from "fs";
25148
+ function flagSet(tier) {
25149
+ return Boolean(getConfigValue(TIER_CONFIG_KEYS[tier]));
25150
+ }
25151
+ function getOnboardTierFlag(tier) {
25152
+ return getConfigValue(TIER_CONFIG_KEYS[tier]);
25153
+ }
25154
+ function markOnboardTierComplete(...tiers) {
25155
+ const at = (/* @__PURE__ */ new Date()).toISOString();
25156
+ for (const tier of tiers) {
25157
+ if (!flagSet(tier)) setConfigValue(TIER_CONFIG_KEYS[tier], at);
25158
+ }
25159
+ }
25160
+ function clearOnboardTierFlags(...tiers) {
25161
+ for (const tier of tiers) {
25162
+ deleteConfigValue(TIER_CONFIG_KEYS[tier]);
25163
+ }
25164
+ }
25165
+ function resetOnboardTierProgress() {
25166
+ clearOnboardTierFlags(...ONBOARD_TIER_ORDER);
25167
+ }
25168
+ function hasProductionDataset(ctx) {
25169
+ const source = ctx?.dataset?.source;
25170
+ if (source && !source.startsWith("demo:")) {
25171
+ if (source.startsWith("/") || /^[A-Za-z]:[\\/]/.test(source) || source.startsWith("~")) {
25172
+ return true;
25173
+ }
25174
+ if (!source.includes(":") && existsSync25(source)) return true;
25175
+ if (source.startsWith("csv:") || source.startsWith("file:") || source.startsWith("folder:")) {
25176
+ return true;
25177
+ }
25178
+ }
25179
+ if (ctx?.attachments && ctx.attachments.length > 0) return true;
25180
+ return flagSet("production");
25181
+ }
25182
+ function hasDemoExperience(ctx) {
25183
+ if (flagSet("demo")) return true;
25184
+ if (getPreferredDemoScenario()) return true;
25185
+ const source = ctx?.dataset?.source;
25186
+ if (source?.startsWith("demo:")) return true;
25187
+ return false;
25188
+ }
25189
+ function listCompletedOnboardTier(ctx) {
25190
+ const done = [];
25191
+ const profileOk = flagSet("profile") || isProfileConfigured(loadProfile());
25192
+ if (profileOk) done.push("profile");
25193
+ else return done;
25194
+ if (flagSet("domain")) done.push("domain");
25195
+ else return done;
25196
+ if (hasDemoExperience(ctx)) done.push("demo");
25197
+ else return done;
25198
+ if (hasProductionDataset(ctx) || flagSet("production")) done.push("production");
25199
+ return done;
25200
+ }
25201
+ function resolveNextOnboardTier(ctx) {
25202
+ const done = new Set(listCompletedOnboardTier(ctx));
25203
+ for (const tier of ONBOARD_TIER_ORDER) {
25204
+ if (!done.has(tier)) return tier;
25205
+ }
25206
+ return null;
25207
+ }
25208
+ function getOnboardTierStatus(ctx) {
25209
+ const completed = listCompletedOnboardTier(ctx);
25210
+ const next = resolveNextOnboardTier(ctx);
25211
+ const meta = next ? TIER_META[next] : null;
25212
+ return {
25213
+ completed,
25214
+ next,
25215
+ nextLabel: meta?.label ?? null,
25216
+ nextHint: meta?.hint ?? null
25217
+ };
25218
+ }
25219
+ function describeOnboardTier(tier) {
25220
+ return TIER_META[tier];
25221
+ }
25222
+ function canRunDomainTier() {
25223
+ return isProfileConfigured(loadProfile()) && hasAnyLlmProvider();
25224
+ }
25225
+ function markProductionDataSeen() {
25226
+ markOnboardTierComplete("production");
25227
+ }
25228
+ function markDemoDataSeen() {
25229
+ markOnboardTierComplete("demo");
25230
+ }
25231
+ function pathLooksPresent(raw) {
25232
+ try {
25233
+ return existsSync25(raw) && (statSync4(raw).isFile() || statSync4(raw).isDirectory());
25234
+ } catch {
25235
+ return false;
25236
+ }
25237
+ }
25238
+ var ONBOARD_TIER_ORDER, TIER_CONFIG_KEYS, TIER_META;
25239
+ var init_onboard_tiers = __esm({
25240
+ "src/conversation/onboard-tiers.ts"() {
25241
+ "use strict";
25242
+ init_store();
25243
+ init_profile();
25244
+ init_repl_api();
25245
+ init_scenario_fit();
25246
+ ONBOARD_TIER_ORDER = [
25247
+ "profile",
25248
+ "domain",
25249
+ "demo",
25250
+ "production"
25251
+ ];
25252
+ TIER_CONFIG_KEYS = {
25253
+ profile: "onboard-tier-profile",
25254
+ domain: "onboard-tier-domain",
25255
+ demo: "onboard-tier-demo",
25256
+ production: "onboard-tier-production"
25257
+ };
25258
+ TIER_META = {
25259
+ profile: {
25260
+ label: "Company profile",
25261
+ hint: "Name, industry, ICP \u2014 works without an API key"
25262
+ },
25263
+ domain: {
25264
+ label: "Domain research",
25265
+ hint: "Connect a key and let NTRP research your company"
25266
+ },
25267
+ demo: {
25268
+ label: "Sample data",
25269
+ hint: "Load a fitted demo book of business"
25270
+ },
25271
+ production: {
25272
+ label: "Your data",
25273
+ hint: "Drag-drop a CSV or folder path into the REPL"
25274
+ }
25275
+ };
25276
+ }
25277
+ });
25278
+
25017
25279
  // src/conversation/voice-setup.ts
25018
25280
  var voice_setup_exports = {};
25019
25281
  __export(voice_setup_exports, {
@@ -25373,97 +25635,314 @@ __export(onboard_exports, {
25373
25635
  profileExists: () => profileExists
25374
25636
  });
25375
25637
  import chalk25 from "chalk";
25638
+ import { existsSync as existsSync26 } from "fs";
25639
+ import { basename as basename7 } from "path";
25376
25640
  async function handler5(args, ctx) {
25377
25641
  const { flags } = parseArgs2(args, ["force", "skip-brand"]);
25378
25642
  const force = getBool(flags, "force");
25379
25643
  const skipBrand = getBool(flags, "skip-brand");
25644
+ const tierOverride = normalizeTierFlag(getString(flags, "tier"));
25380
25645
  const priorProfile = loadProfile();
25381
25646
  const configured = isProfileConfigured(priorProfile);
25382
25647
  let existing = configured ? priorProfile : null;
25383
25648
  if (!skipBrand) printCenteredLogo();
25384
25649
  const session = createPromptSession(ctx.rl, ctx);
25385
25650
  try {
25386
- if (configured && !force) {
25387
- console.log();
25388
- console.log(" " + bold(`Profile already exists for ${priorProfile.company_name}.`));
25389
- const overwrite = await session.confirm("Overwrite this profile?", false);
25390
- if (!overwrite) {
25391
- console.log(" " + chalk25.dim("Keeping existing profile."));
25392
- console.log(" " + chalk25.dim("To skip prompts, type ") + paint("accent", "/onboard --force"));
25393
- return;
25651
+ if (configured && !force && !tierOverride) {
25652
+ const status = getOnboardTierStatus(ctx);
25653
+ if (status.next === null) {
25654
+ console.log();
25655
+ console.log(" " + bold(`You're fully set up for ${priorProfile.company_name}.`));
25656
+ console.log(" " + chalk25.dim("Profile \xB7 domain research \xB7 sample data \xB7 production path \u2014 done."));
25657
+ console.log(
25658
+ " " + chalk25.dim("Type ") + paint("accent", "/onboard --force") + chalk25.dim(" to rebuild the profile, or drop a new CSV anytime.")
25659
+ );
25660
+ console.log();
25661
+ return `Onboarding complete for ${priorProfile.company_name}`;
25394
25662
  }
25663
+ if (status.next === "profile") {
25664
+ console.log();
25665
+ console.log(" " + bold(`Profile already exists for ${priorProfile.company_name}.`));
25666
+ const overwrite = await session.confirm("Overwrite this profile?", false);
25667
+ if (!overwrite) {
25668
+ console.log(" " + chalk25.dim("Keeping existing profile."));
25669
+ return;
25670
+ }
25671
+ existing = null;
25672
+ resetOnboardTierProgress();
25673
+ }
25674
+ } else if (configured && force && !tierOverride) {
25675
+ console.log();
25676
+ console.log(" " + bold(`Rebuilding profile for ${priorProfile.company_name}.`));
25395
25677
  existing = null;
25678
+ resetOnboardTierProgress();
25396
25679
  } else if (priorProfile && !configured) {
25397
25680
  console.log();
25398
- console.log(" " + chalk25.dim("NTRP found an incomplete profile. Setup continues."));
25681
+ console.log(" " + chalk25.dim("NTRP found an incomplete profile. Setup continues from the start."));
25399
25682
  existing = null;
25683
+ resetOnboardTierProgress();
25400
25684
  }
25401
- printIntro();
25402
- await ensureLlmKeys(session);
25403
- const siteOrName = await session.askRequired(
25404
- "What is the company website? Or type the company name."
25405
- );
25406
- const isUrlish = looksLikeUrl(siteOrName);
25407
- const companyUrl = isUrlish ? normalizeUrl(siteOrName) : void 0;
25408
- const seedName = isUrlish ? void 0 : siteOrName.trim();
25409
- const seed = {
25410
- ...companyUrl ? { company_url: companyUrl } : {},
25411
- ...seedName ? { company_name: seedName } : {}
25412
- };
25413
- let draft = await draftCompanyProfile(seed);
25414
- draft = await fillMissingFields(session, draft, existing);
25415
- const canonicalName = draft.company_name ?? seedName ?? existing?.company_name ?? "";
25416
- const salesMotion = draft.sales_motion ?? existing?.sales_motion ?? "mid_market";
25417
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
25418
- let candidate = {
25419
- schema_version: 1,
25420
- company_name: canonicalName,
25421
- ...companyUrl ? { company_url: companyUrl } : existing?.company_url ? { company_url: existing.company_url } : {},
25422
- industry: draft.industry ?? existing?.industry ?? "",
25423
- product_description: draft.product_description ?? existing?.product_description ?? "",
25424
- target_customer: draft.target_customer ?? existing?.target_customer ?? "",
25425
- sales_motion: salesMotion,
25426
- ...draft.average_deal_size !== void 0 ? { average_deal_size: draft.average_deal_size } : {},
25427
- ...draft.sales_cycle_days !== void 0 ? { sales_cycle_days: draft.sales_cycle_days } : {},
25428
- ...draft.primary_crm !== void 0 ? { primary_crm: draft.primary_crm } : {},
25429
- ...draft.engagement_tool !== void 0 ? { engagement_tool: draft.engagement_tool } : {},
25430
- ...existing?.user_scope ? { user_scope: existing.user_scope } : {},
25431
- created_at: existing?.created_at ?? now2,
25432
- updated_at: now2
25433
- };
25434
- renderProfile(candidate);
25435
- candidate = await adaptiveRefine(session, candidate, ctx);
25436
- for (; ; ) {
25437
- renderProfile(candidate);
25438
- const action = (await session.ask("Is this profile correct? Type Y, edit, or redraft", { default: "Y" })).toLowerCase();
25439
- if (action === "y" || action === "yes" || action === "") break;
25440
- if (action === "redraft") {
25441
- const fresh = await fillMissingFields(session, { company_url: candidate.company_url }, null);
25442
- candidate = applyDraft(candidate, fresh);
25443
- continue;
25444
- }
25445
- if (action === "edit" || action === "e") {
25446
- candidate = await editLoop(session, candidate);
25447
- continue;
25448
- }
25449
- console.log(" " + chalk25.red(`Unknown choice: ${action}. Type Y, edit, or redraft.`));
25685
+ const next = tierOverride ?? (force ? "profile" : resolveNextOnboardTier(ctx));
25686
+ if (!next) {
25687
+ console.log();
25688
+ console.log(" " + bold("Onboarding is complete."));
25689
+ console.log(" " + chalk25.dim("Drop a CSV or folder path into the REPL anytime to refresh data."));
25690
+ console.log();
25691
+ return "Onboarding complete";
25692
+ }
25693
+ printTierIntro(next);
25694
+ switch (next) {
25695
+ case "profile":
25696
+ return await runProfileTier(session, ctx, existing);
25697
+ case "domain":
25698
+ return await runDomainTier(session, ctx);
25699
+ case "demo":
25700
+ return await runDemoTier(session, ctx);
25701
+ case "production":
25702
+ return await runProductionTier(session, ctx);
25450
25703
  }
25451
- saveProfile(candidate);
25452
- setConfigValue("sales-motion", candidate.sales_motion);
25453
- invalidateTaxonomy();
25454
- creditOnboardComplete(ctx);
25704
+ } finally {
25705
+ session.close();
25706
+ }
25707
+ }
25708
+ function normalizeTierFlag(raw) {
25709
+ if (!raw) return null;
25710
+ const t = raw.trim().toLowerCase();
25711
+ if (t === "profile" || t === "domain" || t === "demo" || t === "production") return t;
25712
+ return null;
25713
+ }
25714
+ function printTierIntro(tier) {
25715
+ const meta = describeOnboardTier(tier);
25716
+ console.log();
25717
+ console.log(" " + bold(`Onboard \xB7 ${meta.label}`));
25718
+ console.log(" " + chalk25.dim(meta.hint));
25719
+ console.log();
25720
+ }
25721
+ function printNextTierHint(ctx) {
25722
+ const status = getOnboardTierStatus(ctx);
25723
+ if (!status.next) {
25724
+ console.log(" " + chalk25.dim("You're through the progressive setup ladder."));
25725
+ return;
25726
+ }
25727
+ console.log(
25728
+ " " + chalk25.dim("Next: type ") + paint("accent", "/onboard") + chalk25.dim(` for ${status.nextLabel?.toLowerCase()} \u2014 ${status.nextHint}`)
25729
+ );
25730
+ }
25731
+ async function runProfileTier(session, ctx, existing) {
25732
+ console.log(" " + chalk25.dim("No API key needed for this step. Connect one later for domain research."));
25733
+ console.log();
25734
+ const siteOrName = await session.askRequired(
25735
+ "What is the company website? Or type the company name."
25736
+ );
25737
+ const isUrlish = looksLikeUrl(siteOrName);
25738
+ const companyUrl = isUrlish ? normalizeUrl(siteOrName) : void 0;
25739
+ const seedName = isUrlish ? void 0 : siteOrName.trim();
25740
+ const seed = {
25741
+ ...companyUrl ? { company_url: companyUrl } : {},
25742
+ ...seedName ? { company_name: seedName } : {}
25743
+ };
25744
+ let draft = await draftCompanyProfile(seed, ctx, { research: false });
25745
+ draft = await fillMissingFields(session, draft, existing);
25746
+ const canonicalName = draft.company_name ?? seedName ?? existing?.company_name ?? "";
25747
+ const salesMotion = draft.sales_motion ?? existing?.sales_motion ?? "mid_market";
25748
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
25749
+ let candidate = {
25750
+ schema_version: 1,
25751
+ company_name: canonicalName,
25752
+ ...companyUrl ? { company_url: companyUrl } : existing?.company_url ? { company_url: existing.company_url } : {},
25753
+ industry: draft.industry ?? existing?.industry ?? "",
25754
+ product_description: draft.product_description ?? existing?.product_description ?? "",
25755
+ target_customer: draft.target_customer ?? existing?.target_customer ?? "",
25756
+ sales_motion: salesMotion,
25757
+ ...draft.average_deal_size !== void 0 ? { average_deal_size: draft.average_deal_size } : {},
25758
+ ...draft.sales_cycle_days !== void 0 ? { sales_cycle_days: draft.sales_cycle_days } : {},
25759
+ ...draft.primary_crm !== void 0 ? { primary_crm: draft.primary_crm } : {},
25760
+ ...draft.engagement_tool !== void 0 ? { engagement_tool: draft.engagement_tool } : {},
25761
+ ...existing?.user_scope ? { user_scope: existing.user_scope } : {},
25762
+ created_at: existing?.created_at ?? now2,
25763
+ updated_at: now2
25764
+ };
25765
+ candidate = await reviewLoop(session, candidate);
25766
+ saveProfile(candidate);
25767
+ setConfigValue("sales-motion", candidate.sales_motion);
25768
+ invalidateTaxonomy();
25769
+ creditOnboardComplete(ctx);
25770
+ markOnboardTierComplete("profile");
25771
+ if (hasAnyLlmProvider()) {
25455
25772
  console.log();
25456
25773
  console.log(" " + chalk25.green("\u2713") + " " + bold(`Profile saved for ${candidate.company_name}`));
25457
- console.log(" " + chalk25.dim(`~/.ntrp/profile.json`));
25774
+ console.log(" " + chalk25.dim("A key is already connected \u2014 running domain research now."));
25458
25775
  console.log();
25776
+ await runDomainResearchOnProfile(session, ctx, candidate);
25777
+ markOnboardTierComplete("domain");
25459
25778
  await offerInboxSkillSetup(session, { beat: "production" });
25460
- await offerFittedDemoAfterOnboard(session, ctx, candidate);
25461
- const { offerVoiceSetup: offerVoiceSetup2 } = await Promise.resolve().then(() => (init_voice_setup(), voice_setup_exports));
25462
- await offerVoiceSetup2(ctx, session);
25463
- return `Profile saved for ${candidate.company_name}`;
25464
- } finally {
25465
- session.close();
25779
+ const { offerVoiceSetup: offerVoiceSetup3 } = await Promise.resolve().then(() => (init_voice_setup(), voice_setup_exports));
25780
+ await offerVoiceSetup3(ctx, session);
25781
+ printNextTierHint(ctx);
25782
+ return `Profile + domain research saved for ${candidate.company_name}`;
25466
25783
  }
25784
+ console.log();
25785
+ console.log(" " + chalk25.green("\u2713") + " " + bold(`Profile saved for ${candidate.company_name}`));
25786
+ console.log(" " + chalk25.dim(`~/.ntrp/profile.json`));
25787
+ console.log();
25788
+ await offerInboxSkillSetup(session, { beat: "production" });
25789
+ const { offerVoiceSetup: offerVoiceSetup2 } = await Promise.resolve().then(() => (init_voice_setup(), voice_setup_exports));
25790
+ await offerVoiceSetup2(ctx, session);
25791
+ printNextTierHint(ctx);
25792
+ return `Profile saved for ${candidate.company_name}`;
25793
+ }
25794
+ async function runDomainTier(session, ctx) {
25795
+ const profile = loadProfile();
25796
+ if (!isProfileConfigured(profile) || !profile) {
25797
+ console.log(" " + chalk25.dim("Need a company profile first \u2014 starting there."));
25798
+ return runProfileTier(session, ctx, null);
25799
+ }
25800
+ await ensureLlmKeys(session);
25801
+ if (!hasAnyLlmProvider()) {
25802
+ console.log();
25803
+ console.log(" " + chalk25.dim("No key connected \u2014 domain research stays locked."));
25804
+ console.log(
25805
+ " " + chalk25.dim("Type ") + paint("accent", "/connect") + chalk25.dim(" then ") + paint("accent", "/onboard") + chalk25.dim(" to research ") + bold(profile.company_name) + chalk25.dim(".")
25806
+ );
25807
+ console.log();
25808
+ return "Domain research skipped \u2014 no API key";
25809
+ }
25810
+ const updated = await runDomainResearchOnProfile(session, ctx, profile);
25811
+ markOnboardTierComplete("domain");
25812
+ printNextTierHint(ctx);
25813
+ return `Domain research saved for ${updated.company_name}`;
25814
+ }
25815
+ async function runDomainResearchOnProfile(session, ctx, profile) {
25816
+ const seed = {
25817
+ ...profile.company_url ? { company_url: profile.company_url } : {},
25818
+ ...profile.company_name ? { company_name: profile.company_name } : {}
25819
+ };
25820
+ let draft = { ...profile };
25821
+ const spinner = makeSpinner("Researching your company\u2026");
25822
+ try {
25823
+ draft = await draftCompanyProfile(seed, ctx, { research: true });
25824
+ spinner.succeed("Research draft ready");
25825
+ } catch (err) {
25826
+ spinner.fail("Could not research the company");
25827
+ console.log(" " + chalk25.dim(String(err.message ?? err)));
25828
+ console.log(" " + chalk25.dim("Falling back to your existing profile \u2014 you can edit fields below."));
25829
+ draft = { ...profile };
25830
+ }
25831
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
25832
+ let candidate = {
25833
+ ...profile,
25834
+ company_name: draft.company_name ?? profile.company_name,
25835
+ ...draft.company_url || profile.company_url ? { company_url: draft.company_url ?? profile.company_url } : {},
25836
+ industry: draft.industry ?? profile.industry,
25837
+ product_description: draft.product_description ?? profile.product_description,
25838
+ target_customer: draft.target_customer ?? profile.target_customer,
25839
+ sales_motion: draft.sales_motion ?? profile.sales_motion,
25840
+ updated_at: now2
25841
+ };
25842
+ if (draft.average_deal_size !== void 0) candidate.average_deal_size = draft.average_deal_size;
25843
+ else if (profile.average_deal_size) candidate.average_deal_size = profile.average_deal_size;
25844
+ if (draft.sales_cycle_days !== void 0) candidate.sales_cycle_days = draft.sales_cycle_days;
25845
+ else if (profile.sales_cycle_days !== void 0) candidate.sales_cycle_days = profile.sales_cycle_days;
25846
+ if (draft.primary_crm !== void 0) candidate.primary_crm = draft.primary_crm;
25847
+ else if (profile.primary_crm) candidate.primary_crm = profile.primary_crm;
25848
+ if (draft.engagement_tool !== void 0) candidate.engagement_tool = draft.engagement_tool;
25849
+ else if (profile.engagement_tool) candidate.engagement_tool = profile.engagement_tool;
25850
+ const filled = await fillMissingFields(session, candidate, profile);
25851
+ candidate = {
25852
+ ...candidate,
25853
+ company_name: filled.company_name ?? candidate.company_name,
25854
+ industry: filled.industry ?? candidate.industry,
25855
+ product_description: filled.product_description ?? candidate.product_description,
25856
+ target_customer: filled.target_customer ?? candidate.target_customer,
25857
+ ...filled.sales_motion ? { sales_motion: filled.sales_motion } : {},
25858
+ ...filled.average_deal_size !== void 0 ? { average_deal_size: filled.average_deal_size } : {},
25859
+ ...filled.sales_cycle_days !== void 0 ? { sales_cycle_days: filled.sales_cycle_days } : {},
25860
+ ...filled.primary_crm !== void 0 ? { primary_crm: filled.primary_crm } : {},
25861
+ ...filled.engagement_tool !== void 0 ? { engagement_tool: filled.engagement_tool } : {},
25862
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
25863
+ };
25864
+ renderProfile(candidate);
25865
+ candidate = await adaptiveRefine(session, candidate, ctx);
25866
+ candidate = await reviewLoop(session, candidate);
25867
+ saveProfile(candidate);
25868
+ setConfigValue("sales-motion", candidate.sales_motion);
25869
+ invalidateTaxonomy();
25870
+ console.log();
25871
+ console.log(" " + chalk25.green("\u2713") + " " + bold(`Domain research saved for ${candidate.company_name}`));
25872
+ console.log(" " + chalk25.dim(`~/.ntrp/profile.json`));
25873
+ console.log();
25874
+ return candidate;
25875
+ }
25876
+ async function runDemoTier(session, ctx) {
25877
+ const profile = loadProfile();
25878
+ if (!isProfileConfigured(profile) || !profile) {
25879
+ console.log(" " + chalk25.dim("Need a company profile first \u2014 starting there."));
25880
+ return runProfileTier(session, ctx, null);
25881
+ }
25882
+ console.log(" " + chalk25.dim("Fit a sample book of business shaped like yours \u2014 or skip to bring your own later."));
25883
+ console.log();
25884
+ const loaded2 = await offerFittedDemoAfterOnboard(session, ctx, profile);
25885
+ markDemoDataSeen();
25886
+ if (loaded2) {
25887
+ console.log();
25888
+ printNextTierHint(ctx);
25889
+ return "Demo data loaded";
25890
+ }
25891
+ console.log();
25892
+ console.log(" " + chalk25.dim("Skipped sample data. Next up is your production CSV or folder."));
25893
+ printNextTierHint(ctx);
25894
+ return "Demo data skipped";
25895
+ }
25896
+ async function runProductionTier(session, ctx) {
25897
+ console.log(" " + bold("Bring your own pipeline data"));
25898
+ console.log(" " + chalk25.dim("Drag a CSV (or a folder of CSVs) onto this terminal \u2014 same as dropping a path in a shell."));
25899
+ console.log(" " + chalk25.dim("NTRP will read the path and ingest what it can. Or type the path and press Enter."));
25900
+ console.log();
25901
+ const raw = await session.ask("CSV or folder path. Press Enter to skip for now", { default: "" });
25902
+ const trimmed = raw.trim().replace(/^["']|["']$/g, "");
25903
+ if (!trimmed) {
25904
+ markProductionDataSeen();
25905
+ console.log();
25906
+ console.log(
25907
+ " " + chalk25.dim("Skipped. Drop a file path into the REPL anytime \u2014 NTRP will pick it up.")
25908
+ );
25909
+ console.log();
25910
+ return "Production data skipped";
25911
+ }
25912
+ const resolved = resolveUserPath(trimmed);
25913
+ if (!existsSync26(resolved)) {
25914
+ console.log(" " + chalk25.red(`Path not found: ${resolved}`));
25915
+ console.log(" " + chalk25.dim("Try again with /onboard, or drop the path into the REPL."));
25916
+ return "Production path not found";
25917
+ }
25918
+ const { ingestPathFromChat: ingestPathFromChat2 } = await Promise.resolve().then(() => (init_ingest_chat(), ingest_chat_exports));
25919
+ const ok = await ingestPathFromChat2(ctx, resolved);
25920
+ if (ok) {
25921
+ markProductionDataSeen();
25922
+ printNextTierHint(ctx);
25923
+ return `Production data loaded from ${basename7(resolved)}`;
25924
+ }
25925
+ console.log(" " + chalk25.dim("Nothing ingested. Drop a CSV path into the REPL when ready."));
25926
+ return "Production ingest cancelled";
25927
+ }
25928
+ async function reviewLoop(session, initial) {
25929
+ let candidate = initial;
25930
+ for (; ; ) {
25931
+ renderProfile(candidate);
25932
+ const action = (await session.ask("Is this profile correct? Type Y, edit, or redraft", { default: "Y" })).toLowerCase();
25933
+ if (action === "y" || action === "yes" || action === "") break;
25934
+ if (action === "redraft") {
25935
+ const fresh = await fillMissingFields(session, { company_url: candidate.company_url }, null);
25936
+ candidate = applyDraft(candidate, fresh);
25937
+ continue;
25938
+ }
25939
+ if (action === "edit" || action === "e") {
25940
+ candidate = await editLoop(session, candidate);
25941
+ continue;
25942
+ }
25943
+ console.log(" " + chalk25.red(`Unknown choice: ${action}. Type Y, edit, or redraft.`));
25944
+ }
25945
+ return candidate;
25467
25946
  }
25468
25947
  async function fillMissingFields(session, draft, existing) {
25469
25948
  const out = { ...draft };
@@ -25685,16 +26164,10 @@ async function editField(session, p, field) {
25685
26164
  }
25686
26165
  }
25687
26166
  }
25688
- function printIntro() {
25689
- console.log(" " + bold("Let's set up your company profile."));
25690
- console.log(" " + chalk25.dim("NTRP will research your business so every answer,"));
25691
- console.log(" " + chalk25.dim("finding, and demo is tailored to your reality."));
25692
- console.log();
25693
- }
25694
26167
  function printKeylessNote() {
25695
- console.log(" " + chalk25.dim("Skipping AI \u2014 I'll ask a few quick questions instead."));
26168
+ console.log(" " + chalk25.dim("Skipping AI \u2014 domain research needs a key."));
25696
26169
  console.log(
25697
- " " + chalk25.dim("Add a key anytime with ") + paint("accent", "/connect") + chalk25.dim(" to unlock research, findings, and chat answers.")
26170
+ " " + chalk25.dim("Add a key anytime with ") + paint("accent", "/connect") + chalk25.dim(" then type ") + paint("accent", "/onboard") + chalk25.dim(".")
25698
26171
  );
25699
26172
  console.log();
25700
26173
  }
@@ -25704,9 +26177,9 @@ async function ensureLlmKeys(session) {
25704
26177
  const { providerLabel: providerLabel2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
25705
26178
  const { countAvailableEngines: countAvailableEngines2 } = await Promise.resolve().then(() => (init_session_state(), session_state_exports));
25706
26179
  console.log();
25707
- console.log(" " + bold("Connect a key") + chalk25.dim(" (optional. Press Enter to skip)"));
25708
- console.log(" " + chalk25.dim("Scores and metrics are local math. No AI is required."));
25709
- console.log(" " + chalk25.dim("A key adds company research now. It also adds findings and chat answers later."));
26180
+ console.log(" " + bold("Connect a key") + chalk25.dim(" (needed for domain research. Press Enter to skip)"));
26181
+ console.log(" " + chalk25.dim("Scores and metrics are local math. No AI is required for vitals."));
26182
+ console.log(" " + chalk25.dim("A key unlocks company research now, plus findings and chat answers later."));
25710
26183
  console.log(
25711
26184
  " " + chalk25.dim("Paste any provider API key. Anthropic, OpenAI, Groq, Gemini, Mistral, and others.")
25712
26185
  );
@@ -25798,6 +26271,8 @@ var init_onboard = __esm({
25798
26271
  init_profile_clarify();
25799
26272
  init_profile2();
25800
26273
  init_inbox_setup();
26274
+ init_onboard_tiers();
26275
+ init_path_safety();
25801
26276
  SALES_MOTION_CHOICES = [
25802
26277
  { value: "plg", label: "PLG \u2014 Product-Led Growth", description: PRESET_DESCRIPTIONS.plg },
25803
26278
  { value: "smb_velocity", label: "SMB Velocity \u2014 Fast cycles, high volume", description: PRESET_DESCRIPTIONS.smb_velocity },
@@ -25838,8 +26313,8 @@ __export(new_exports, {
25838
26313
  handler: () => handler6
25839
26314
  });
25840
26315
  import chalk26 from "chalk";
25841
- import { existsSync as existsSync25 } from "fs";
25842
- import { basename as basename7 } from "path";
26316
+ import { existsSync as existsSync27 } from "fs";
26317
+ import { basename as basename8 } from "path";
25843
26318
  async function handler6(args, ctx) {
25844
26319
  const { positional, flags } = parseArgs2(args, ["demo", "empty", "list-scenarios", "regen-taxonomy"]);
25845
26320
  let source = null;
@@ -25860,7 +26335,7 @@ async function handler6(args, ctx) {
25860
26335
  console.error(chalk26.red(" Usage: /new <file.csv> | --demo [--scenario <name>] | --empty [--lens health|metrics]"));
25861
26336
  return;
25862
26337
  }
25863
- if (source.kind === "file" && !existsSync25(source.path)) {
26338
+ if (source.kind === "file" && !existsSync27(source.path)) {
25864
26339
  console.error(chalk26.red(` File not found: ${source.path}`));
25865
26340
  return;
25866
26341
  }
@@ -25884,7 +26359,7 @@ async function handler6(args, ctx) {
25884
26359
  const sourceFlag = getString(flags, "source", "s");
25885
26360
  if (sourceFlag) passthrough.push("--source", sourceFlag);
25886
26361
  await ingest(passthrough, ctx);
25887
- datasetLabel2 = basename7(source.path);
26362
+ datasetLabel2 = basename8(source.path);
25888
26363
  datasetSource = source.path;
25889
26364
  } else if (source.kind === "demo") {
25890
26365
  const { handler: demo } = await Promise.resolve().then(() => (init_generate(), generate_exports));
@@ -26087,7 +26562,7 @@ __export(end_exports, {
26087
26562
  handler: () => handler7
26088
26563
  });
26089
26564
  import chalk27 from "chalk";
26090
- import { existsSync as existsSync26 } from "fs";
26565
+ import { existsSync as existsSync28 } from "fs";
26091
26566
  async function handler7(args, ctx) {
26092
26567
  if (args.length > 0) {
26093
26568
  console.error(chalk27.red(" Usage: /end"));
@@ -26124,10 +26599,10 @@ async function handler7(args, ctx) {
26124
26599
  if (summary) {
26125
26600
  console.log(" " + chalk27.dim(summary));
26126
26601
  }
26127
- if (existsSync26(transcriptPathForSession(endedId))) {
26602
+ if (existsSync28(transcriptPathForSession(endedId))) {
26128
26603
  console.log(" " + chalk27.dim("Transcript: ") + chalk27.dim(transcriptPathForSession(endedId)));
26129
26604
  }
26130
- if (existsSync26(contextDocPathForSession(endedId))) {
26605
+ if (existsSync28(contextDocPathForSession(endedId))) {
26131
26606
  console.log(" " + chalk27.dim("Context brief: ") + chalk27.dim(contextDocPathForSession(endedId)));
26132
26607
  }
26133
26608
  console.log();
@@ -26149,7 +26624,7 @@ __export(session_exports, {
26149
26624
  });
26150
26625
  import chalk28 from "chalk";
26151
26626
  import { join as join23 } from "path";
26152
- import { existsSync as existsSync27 } from "fs";
26627
+ import { existsSync as existsSync29 } from "fs";
26153
26628
  async function handler8(args, ctx) {
26154
26629
  const sub = args[0];
26155
26630
  if (!sub) return listSessionsView(ctx);
@@ -26295,7 +26770,7 @@ async function pickUp(idArg, ctx) {
26295
26770
  );
26296
26771
  }
26297
26772
  const contextPath = contextDocPathForSession(target.id);
26298
- if (existsSync27(contextPath)) {
26773
+ if (existsSync29(contextPath)) {
26299
26774
  console.log(" " + chalk28.dim("Context brief: ") + chalk28.dim(contextPath));
26300
26775
  }
26301
26776
  console.log();
@@ -27247,7 +27722,7 @@ var init_bundle = __esm({
27247
27722
 
27248
27723
  // src/repositories/markdown.ts
27249
27724
  import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync17 } from "fs";
27250
- import { basename as basename8, dirname as dirname5, join as join26, resolve as resolve8 } from "path";
27725
+ import { basename as basename9, dirname as dirname5, join as join26, resolve as resolve8 } from "path";
27251
27726
  import { stringify as stringifyYaml2 } from "yaml";
27252
27727
  function renderMarkdownFiles(pkg) {
27253
27728
  const bundleJson = JSON.stringify(pkg, null, 2) + "\n";
@@ -27437,7 +27912,7 @@ function getRootPath(target) {
27437
27912
  return resolve8(target.directory ?? `ntrp-repository-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
27438
27913
  }
27439
27914
  function safeFilename(value) {
27440
- return (basename8(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "strategy") + ".md";
27915
+ return (basename9(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "strategy") + ".md";
27441
27916
  }
27442
27917
  function escapeSummary(value) {
27443
27918
  return value.replace(/[<>]/g, "");
@@ -31967,7 +32442,7 @@ var init_checkout = __esm({
31967
32442
  });
31968
32443
 
31969
32444
  // src/services/setup.ts
31970
- import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync20, writeFileSync as writeFileSync19 } from "fs";
32445
+ import { existsSync as existsSync30, mkdirSync as mkdirSync17, readFileSync as readFileSync20, writeFileSync as writeFileSync19 } from "fs";
31971
32446
  import { join as join29 } from "path";
31972
32447
  function setupCheck() {
31973
32448
  const home = ntrpHome();
@@ -33403,7 +33878,7 @@ var init_metrics = __esm({
33403
33878
  });
33404
33879
 
33405
33880
  // src/ai/feedback-apply.ts
33406
- function stripFences4(text) {
33881
+ function stripFences5(text) {
33407
33882
  const trimmed = text.trim();
33408
33883
  const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
33409
33884
  if (fenced) return fenced[1].trim();
@@ -33433,8 +33908,8 @@ OPERATOR FEEDBACK:
33433
33908
  "${feedbackText}"
33434
33909
 
33435
33910
  Apply the feedback now as STRICT JSON.`;
33436
- const { text } = await llmCompleteText("feedback", SYSTEM_PROMPT4, userMessage, 1024, ctx);
33437
- const cleaned = stripFences4(text);
33911
+ const { text } = await llmCompleteText("feedback", SYSTEM_PROMPT5, userMessage, 1024, ctx);
33912
+ const cleaned = stripFences5(text);
33438
33913
  let parsed;
33439
33914
  try {
33440
33915
  parsed = JSON.parse(cleaned);
@@ -33461,7 +33936,7 @@ function validatePatch(raw) {
33461
33936
  const targetCustomer = str2("target_customer");
33462
33937
  if (targetCustomer) out.target_customer = targetCustomer;
33463
33938
  const motion = str2("sales_motion")?.toLowerCase();
33464
- if (motion && ALLOWED_MOTIONS2.has(motion)) {
33939
+ if (motion && ALLOWED_MOTIONS3.has(motion)) {
33465
33940
  out.sales_motion = motion;
33466
33941
  }
33467
33942
  const dealSize = str2("average_deal_size");
@@ -33471,25 +33946,25 @@ function validatePatch(raw) {
33471
33946
  out.sales_cycle_days = Math.round(cycleDays);
33472
33947
  }
33473
33948
  const crm = str2("primary_crm")?.toLowerCase();
33474
- if (crm && ALLOWED_CRMS2.has(crm)) out.primary_crm = crm;
33949
+ if (crm && ALLOWED_CRMS3.has(crm)) out.primary_crm = crm;
33475
33950
  const engagement = str2("engagement_tool")?.toLowerCase();
33476
- if (engagement && ALLOWED_ENGAGEMENT2.has(engagement)) out.engagement_tool = engagement;
33951
+ if (engagement && ALLOWED_ENGAGEMENT3.has(engagement)) out.engagement_tool = engagement;
33477
33952
  const userScope = str2("user_scope");
33478
33953
  if (userScope) out.user_scope = userScope;
33479
33954
  const customContext = str2("custom_context");
33480
33955
  if (customContext) out.custom_context = customContext;
33481
33956
  return out;
33482
33957
  }
33483
- var ALLOWED_MOTIONS2, ALLOWED_CRMS2, ALLOWED_ENGAGEMENT2, SYSTEM_PROMPT4;
33958
+ var ALLOWED_MOTIONS3, ALLOWED_CRMS3, ALLOWED_ENGAGEMENT3, SYSTEM_PROMPT5;
33484
33959
  var init_feedback_apply = __esm({
33485
33960
  "src/ai/feedback-apply.ts"() {
33486
33961
  "use strict";
33487
33962
  init_repl_api();
33488
33963
  init_complete();
33489
- ALLOWED_MOTIONS2 = /* @__PURE__ */ new Set(["plg", "smb_velocity", "mid_market", "enterprise"]);
33490
- ALLOWED_CRMS2 = /* @__PURE__ */ new Set(["salesforce", "hubspot", "pipedrive", "other"]);
33491
- ALLOWED_ENGAGEMENT2 = /* @__PURE__ */ new Set(["outreach", "salesloft", "apollo", "none"]);
33492
- SYSTEM_PROMPT4 = `You are a profile-correction engine for NTRP, a GTM pipeline-health tool. The operator is giving you natural-language feedback to correct or augment their company profile.
33964
+ ALLOWED_MOTIONS3 = /* @__PURE__ */ new Set(["plg", "smb_velocity", "mid_market", "enterprise"]);
33965
+ ALLOWED_CRMS3 = /* @__PURE__ */ new Set(["salesforce", "hubspot", "pipedrive", "other"]);
33966
+ ALLOWED_ENGAGEMENT3 = /* @__PURE__ */ new Set(["outreach", "salesloft", "apollo", "none"]);
33967
+ SYSTEM_PROMPT5 = `You are a profile-correction engine for NTRP, a GTM pipeline-health tool. The operator is giving you natural-language feedback to correct or augment their company profile.
33493
33968
 
33494
33969
  YOUR JOB:
33495
33970
  1. Map feedback to STRUCTURED profile fields when there's a clear match (e.g. "our sales cycle is 6 months" \u2192 sales_cycle_days: 180).
@@ -33974,7 +34449,7 @@ __export(sessions_exports, {
33974
34449
  handler: () => handler35
33975
34450
  });
33976
34451
  import chalk62 from "chalk";
33977
- import { existsSync as existsSync29 } from "fs";
34452
+ import { existsSync as existsSync31 } from "fs";
33978
34453
  async function handler35(args, _ctx) {
33979
34454
  const sub = args[0] ?? "list";
33980
34455
  if (sub === "list" || !args[0]) {
@@ -34081,12 +34556,12 @@ function showSession(idArg) {
34081
34556
  console.log();
34082
34557
  const transcriptPath = transcriptPathForSession(session.id);
34083
34558
  const contextPath = contextDocPathForSession(session.id);
34084
- if (existsSync29(transcriptPath) || existsSync29(contextPath)) {
34559
+ if (existsSync31(transcriptPath) || existsSync31(contextPath)) {
34085
34560
  console.log(" " + chalk62.dim("\u2500".repeat(40)));
34086
- if (existsSync29(contextPath)) {
34561
+ if (existsSync31(contextPath)) {
34087
34562
  console.log(" " + chalk62.dim("Context brief: ") + chalk62.dim(contextPath));
34088
34563
  }
34089
- if (existsSync29(transcriptPath)) {
34564
+ if (existsSync31(transcriptPath)) {
34090
34565
  console.log(" " + chalk62.dim("Full transcript: ") + chalk62.dim(transcriptPath));
34091
34566
  }
34092
34567
  console.log();
@@ -34355,6 +34830,7 @@ var init_privacy_notice = __esm({
34355
34830
  "Direct identifiers (names, emails, domains, deal names) are replaced with local tokens before any LLM HTTP call.",
34356
34831
  "The mapping stays in ~/.ntrp/privacy/ on this machine. The CLI shows real names; the provider never does.",
34357
34832
  "This is pseudonymization, not anonymization \u2014 you can reverse it; the model cannot.",
34833
+ "Exception: /onboard domain research sends the company name and website you typed so the model can draft your profile. That is operator-consented and only that step.",
34358
34834
  "Computed scores and dollar aggregates still go to the provider you connected.",
34359
34835
  "If you turn on web retrieval, named-account queries are refused. Generic GTM terms may go to Tavily or Brave.",
34360
34836
  "A custom --base-url receives the same tokenized payload.",
@@ -35422,20 +35898,20 @@ var init_model = __esm({
35422
35898
  });
35423
35899
 
35424
35900
  // src/config/update-check.ts
35425
- import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync21, unlinkSync as unlinkSync5, writeFileSync as writeFileSync20 } from "fs";
35901
+ import { existsSync as existsSync32, mkdirSync as mkdirSync18, readFileSync as readFileSync21, unlinkSync as unlinkSync5, writeFileSync as writeFileSync20 } from "fs";
35426
35902
  import { join as join33 } from "path";
35427
35903
  function cachePath2() {
35428
35904
  return join33(ntrpHome(), "update-check.json");
35429
35905
  }
35430
35906
  function ensureDir7() {
35431
35907
  const dir = ntrpHome();
35432
- if (!existsSync30(dir)) {
35908
+ if (!existsSync32(dir)) {
35433
35909
  mkdirSync18(dir, { recursive: true });
35434
35910
  }
35435
35911
  }
35436
35912
  function loadUpdateCheckCache() {
35437
35913
  const path = cachePath2();
35438
- if (!existsSync30(path)) return null;
35914
+ if (!existsSync32(path)) return null;
35439
35915
  try {
35440
35916
  const parsed = JSON.parse(readFileSync21(path, "utf-8"));
35441
35917
  if (!parsed || typeof parsed !== "object" || typeof parsed.lastCheck !== "number" || typeof parsed.latestVersion !== "string") {
@@ -35456,7 +35932,7 @@ function isCacheFresh(cache2, ttlMs = CACHE_TTL_MS2) {
35456
35932
  }
35457
35933
  function invalidateUpdateCheckCache() {
35458
35934
  const path = cachePath2();
35459
- if (existsSync30(path)) {
35935
+ if (existsSync32(path)) {
35460
35936
  unlinkSync5(path);
35461
35937
  }
35462
35938
  }
@@ -35470,11 +35946,11 @@ var init_update_check = __esm({
35470
35946
  });
35471
35947
 
35472
35948
  // src/version.ts
35473
- import { existsSync as existsSync31, readFileSync as readFileSync22 } from "fs";
35949
+ import { existsSync as existsSync33, readFileSync as readFileSync22 } from "fs";
35474
35950
  import { dirname as dirname6, join as join34 } from "path";
35475
35951
  import { fileURLToPath } from "url";
35476
35952
  function readVersionFromPackageJson(packageJsonPath) {
35477
- if (!existsSync31(packageJsonPath)) return null;
35953
+ if (!existsSync33(packageJsonPath)) return null;
35478
35954
  try {
35479
35955
  const pkg = JSON.parse(readFileSync22(packageJsonPath, "utf-8"));
35480
35956
  if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
@@ -35614,7 +36090,7 @@ __export(relaunch_exports, {
35614
36090
  resolveRelaunchEntry: () => resolveRelaunchEntry,
35615
36091
  updateRestartSummary: () => updateRestartSummary
35616
36092
  });
35617
- import { existsSync as existsSync32 } from "fs";
36093
+ import { existsSync as existsSync34 } from "fs";
35618
36094
  import { join as join35 } from "path";
35619
36095
  import { fileURLToPath as fileURLToPath2 } from "url";
35620
36096
  import { spawnSync } from "child_process";
@@ -35640,7 +36116,7 @@ function npmGlobalEntry() {
35640
36116
  const listed = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
35641
36117
  if (listed.status !== 0) return null;
35642
36118
  const entry = join35(listed.stdout.trim(), NPM_PACKAGE, "dist/index.js");
35643
- return existsSync32(entry) ? entry : null;
36119
+ return existsSync34(entry) ? entry : null;
35644
36120
  }
35645
36121
  function thisBundleEntry() {
35646
36122
  return fileURLToPath2(import.meta.url);
@@ -35650,10 +36126,10 @@ function resolveRelaunchEntry(toVersion) {
35650
36126
  (p) => Boolean(p)
35651
36127
  );
35652
36128
  for (const entry of candidates) {
35653
- if (!existsSync32(entry)) continue;
36129
+ if (!existsSync34(entry)) continue;
35654
36130
  if (readVersionNearEntry(entry) === toVersion) return entry;
35655
36131
  }
35656
- return candidates.find((p) => existsSync32(p)) ?? thisBundleEntry();
36132
+ return candidates.find((p) => existsSync34(p)) ?? thisBundleEntry();
35657
36133
  }
35658
36134
  function relaunchArgv(toVersion) {
35659
36135
  return [resolveRelaunchEntry(toVersion)];
@@ -36176,7 +36652,7 @@ __export(exports_exports, {
36176
36652
  handler: () => handler48
36177
36653
  });
36178
36654
  import chalk78 from "chalk";
36179
- import { existsSync as existsSync33 } from "fs";
36655
+ import { existsSync as existsSync35 } from "fs";
36180
36656
  import { join as join36 } from "path";
36181
36657
  function usage4() {
36182
36658
  console.log(chalk78.dim(" Usage:"));
@@ -36335,7 +36811,7 @@ function runMove(args, ctx) {
36335
36811
  }
36336
36812
  try {
36337
36813
  const destDir = resolveUserPath(dest);
36338
- if (!existsSync33(destDir)) {
36814
+ if (!existsSync35(destDir)) {
36339
36815
  }
36340
36816
  const event = moveExport(idOrName, destDir);
36341
36817
  console.log();
@@ -36707,10 +37183,9 @@ section: Settings
36707
37183
  handler: ../commands/onboard.ts
36708
37184
  ---
36709
37185
 
36710
- Start the first-run wizard. It builds a company profile.
36711
- Connect one or two keys (Anthropic, OpenAI, or another provider).
36712
- Then answer a few seed questions. AI drafts industry, ICP, deal size, and stack guesses.
36713
- 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.
37186
+ Start the progressive setup ladder. Each \`/onboard\` runs the next incomplete tier:
37187
+ profile (keyless) \u2192 domain research + API key \u2192 sample demo data \u2192 production CSV/folder path.
37188
+ Drag-drop a CSV or folder into the REPL anytime for real data.
36714
37189
  The profile is stored at \`~/.ntrp/profile.json\`. It flows into findings, NL answers, and demo data.`
36715
37190
  },
36716
37191
  {
@@ -37370,7 +37845,7 @@ Remaining nuances merge into a custom_context paragraph that flows into all AI s
37370
37845
  });
37371
37846
 
37372
37847
  // src/ai/prompt-parts.ts
37373
- import { existsSync as existsSync34, readFileSync as readFileSync23 } from "fs";
37848
+ import { existsSync as existsSync36, readFileSync as readFileSync23 } from "fs";
37374
37849
  import { join as join37 } from "path";
37375
37850
  function buildCompanyProfileBlock() {
37376
37851
  const p = loadProfile();
@@ -37391,7 +37866,7 @@ function buildCompanyProfileBlock() {
37391
37866
  function loadAnalystFile() {
37392
37867
  const path = join37(ntrpHome(), ANALYST_FILE_NAME);
37393
37868
  try {
37394
- if (!existsSync34(path)) return null;
37869
+ if (!existsSync36(path)) return null;
37395
37870
  const raw = sanitizeExternalText(readFileSync23(path, "utf-8").trim());
37396
37871
  if (!raw) return null;
37397
37872
  if (raw.length <= ANALYST_FILE_MAX_CHARS) return raw;
@@ -37920,39 +38395,54 @@ var ingest_chat_exports = {};
37920
38395
  __export(ingest_chat_exports, {
37921
38396
  extractFilePath: () => extractFilePath,
37922
38397
  ingestFromChat: () => ingestFromChat,
38398
+ ingestPathFromChat: () => ingestPathFromChat,
37923
38399
  isDemoIntent: () => isDemoIntent,
38400
+ listCsvsInFolder: () => listCsvsInFolder,
37924
38401
  loadDemoFromChat: () => loadDemoFromChat,
37925
38402
  looksLikeFilePath: () => looksLikeFilePath
37926
38403
  });
37927
- import { existsSync as existsSync35 } from "fs";
37928
- import { basename as basename9, resolve as resolve9 } from "path";
38404
+ import { existsSync as existsSync37, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
38405
+ import { basename as basename10, join as join38, resolve as resolve9 } from "path";
37929
38406
  import { homedir as homedir8 } from "os";
37930
38407
  import chalk80 from "chalk";
37931
38408
  function extractFilePath(input) {
37932
- const trimmed = input.trim();
38409
+ const trimmed = input.trim().replace(/^["']|["']$/g, "");
38410
+ if (!trimmed) return null;
37933
38411
  const patterns = [
37934
- /^["'](.+\.csv)["']$/i,
37935
- /^@(.+\.csv)$/i,
37936
- /(?:here'?s|file|path|upload)[:\s]+["']?([^\s"']+\.csv)["']?/i,
37937
- /^~\/\S+\.csv$/i,
37938
- /^\.\.?\/\S+\.csv$/i,
37939
- /^\/\S+\.csv$/i,
37940
- /^[A-Za-z]:\\[^\s]+\.csv$/i,
37941
- /^[^\s]+\.csv$/i
38412
+ /^["'](.+)["']$/i,
38413
+ /^@(.+)$/i,
38414
+ /(?:here'?s|file|path|upload|folder|dir)[:\s]+["']?([^\s"']+)["']?/i,
38415
+ /^~\/\S+$/i,
38416
+ /^\.\.?\/\S+$/i,
38417
+ /^\/\S+$/i,
38418
+ /^[A-Za-z]:\\[^\s]+$/i,
38419
+ /^[^\s]+$/i
37942
38420
  ];
37943
38421
  for (const re of patterns) {
37944
38422
  const m = trimmed.match(re);
37945
- if (m?.[1]) {
37946
- const p = expandPath(m[1]);
37947
- if (existsSync35(p)) return p;
37948
- }
37949
- if (!m?.[1] && re.test(trimmed) && trimmed.toLowerCase().endsWith(".csv")) {
37950
- const p = expandPath(trimmed.replace(/^["']|["']$/g, ""));
37951
- if (existsSync35(p)) return p;
38423
+ const candidate = (m?.[1] ?? (re.test(trimmed) ? trimmed : null))?.replace(/^["']|["']$/g, "");
38424
+ if (!candidate) continue;
38425
+ if (!looksLikePathToken(candidate)) continue;
38426
+ const p = expandPath(candidate);
38427
+ if (existsSync37(p)) {
38428
+ try {
38429
+ const st = statSync5(p);
38430
+ if (st.isFile() || st.isDirectory()) return p;
38431
+ } catch {
38432
+ }
37952
38433
  }
37953
38434
  }
37954
38435
  return null;
37955
38436
  }
38437
+ function looksLikePathToken(token) {
38438
+ if (token.length < 2) return false;
38439
+ if (/^(yes|no|y|n|back|b|prev|cancel|skip|help|demo)$/i.test(token)) return false;
38440
+ if (token.includes("/") || token.includes("\\")) return true;
38441
+ if (token.startsWith("~")) return true;
38442
+ if (/^[A-Za-z]:/.test(token)) return true;
38443
+ if (/\.[A-Za-z0-9]{1,8}$/.test(token)) return true;
38444
+ return false;
38445
+ }
37956
38446
  function expandPath(p) {
37957
38447
  if (p.startsWith("~/")) return resolve9(homedir8(), p.slice(2));
37958
38448
  return resolve9(p);
@@ -37960,12 +38450,79 @@ function expandPath(p) {
37960
38450
  function looksLikeFilePath(input) {
37961
38451
  return extractFilePath(input) !== null;
37962
38452
  }
38453
+ function listCsvsInFolder(dir) {
38454
+ try {
38455
+ if (!statSync5(dir).isDirectory()) return [];
38456
+ return readdirSync6(dir).filter((name) => name.toLowerCase().endsWith(".csv")).map((name) => join38(dir, name)).sort();
38457
+ } catch {
38458
+ return [];
38459
+ }
38460
+ }
38461
+ async function ingestPathFromChat(ctx, rawPath) {
38462
+ let st;
38463
+ try {
38464
+ st = statSync5(rawPath);
38465
+ } catch {
38466
+ console.log(" " + chalk80.red(`Path not found: ${rawPath}`));
38467
+ return false;
38468
+ }
38469
+ if (st.isDirectory()) {
38470
+ const csvs = listCsvsInFolder(rawPath);
38471
+ if (csvs.length === 0) {
38472
+ console.log();
38473
+ console.log(" " + paint("accent", "Folder noted") + chalk80.dim(` \u2014 ${rawPath}`));
38474
+ console.log(" " + chalk80.dim("No CSV files one level deep. Drop a .csv path, or put exports in that folder."));
38475
+ ctx.attachments = [
38476
+ ...ctx.attachments ?? [],
38477
+ { path: rawPath, ingested_at: (/* @__PURE__ */ new Date()).toISOString() }
38478
+ ];
38479
+ saveSessionState(ctx);
38480
+ markProductionDataSeen();
38481
+ return false;
38482
+ }
38483
+ if (csvs.length === 1) {
38484
+ console.log(" " + chalk80.dim(`Found ${basename10(csvs[0])} in folder \u2014 ingesting.`));
38485
+ return ingestFromChat(ctx, csvs[0]);
38486
+ }
38487
+ if (!ctx.rl) {
38488
+ console.log(" " + chalk80.dim(`Found ${csvs.length} CSVs \u2014 re-run interactively to pick one.`));
38489
+ return false;
38490
+ }
38491
+ const prompts = createPromptSession(ctx.rl, ctx);
38492
+ try {
38493
+ const picked = await prompts.choose(
38494
+ "Which CSV in that folder?",
38495
+ csvs.map((p) => ({ value: p, label: basename10(p), description: p })),
38496
+ { default: csvs[0] }
38497
+ );
38498
+ return ingestFromChat(ctx, picked);
38499
+ } finally {
38500
+ prompts.close();
38501
+ }
38502
+ }
38503
+ if (st.isFile()) {
38504
+ if (!rawPath.toLowerCase().endsWith(".csv")) {
38505
+ console.log();
38506
+ console.log(" " + paint("accent", "Path noted") + chalk80.dim(` \u2014 ${rawPath}`));
38507
+ console.log(" " + chalk80.dim("NTRP ingests CSV exports today. Drop a .csv from that location when ready."));
38508
+ ctx.attachments = [
38509
+ ...ctx.attachments ?? [],
38510
+ { path: rawPath, ingested_at: (/* @__PURE__ */ new Date()).toISOString() }
38511
+ ];
38512
+ saveSessionState(ctx);
38513
+ markProductionDataSeen();
38514
+ return false;
38515
+ }
38516
+ return ingestFromChat(ctx, rawPath);
38517
+ }
38518
+ return false;
38519
+ }
37963
38520
  async function ingestFromChat(ctx, filePath) {
37964
38521
  if (!ctx.rl) {
37965
38522
  console.log(" " + chalk80.red("Ingest confirm requires interactive mode."));
37966
38523
  return false;
37967
38524
  }
37968
- const name = basename9(filePath);
38525
+ const name = basename10(filePath);
37969
38526
  const prompts = createPromptSession(ctx.rl, ctx);
37970
38527
  try {
37971
38528
  const ok = await prompts.confirm(`Ingest ${name} as CRM export?`, true);
@@ -38024,6 +38581,7 @@ async function ingestFromChat(ctx, filePath) {
38024
38581
  };
38025
38582
  invalidateGapAudit(ctx);
38026
38583
  saveSessionState(ctx);
38584
+ markProductionDataSeen();
38027
38585
  console.log();
38028
38586
  console.log(" " + paint("accent", "\u2713 Data loaded") + chalk80.dim(` \u2014 ${name}`));
38029
38587
  recordMessage(ctx, "user", `[ingested ${name}]`);
@@ -38099,6 +38657,7 @@ async function loadDemoFromChat(ctx, scenario, opts = {}) {
38099
38657
  counts,
38100
38658
  ingested_at: (/* @__PURE__ */ new Date()).toISOString()
38101
38659
  };
38660
+ markDemoDataSeen();
38102
38661
  if (!ctx.scope) {
38103
38662
  const { proposeScopeFromIntent: proposeScopeFromIntent2 } = await Promise.resolve().then(() => (init_scope(), scope_exports));
38104
38663
  const proposal = proposeScopeFromIntent2("demo pipeline and metrics");
@@ -38135,6 +38694,7 @@ var init_ingest_chat = __esm({
38135
38694
  init_compute2();
38136
38695
  init_theme();
38137
38696
  init_demo();
38697
+ init_onboard_tiers();
38138
38698
  }
38139
38699
  });
38140
38700
 
@@ -38482,7 +39042,7 @@ async function runFirstRunFork(ctx, options = {}) {
38482
39042
  }
38483
39043
  function printFirstRunChip() {
38484
39044
  console.log(
38485
- " " + chalk82.dim("No profile yet \u2014 ask a question, type ") + chalk82.cyan("use demo data") + chalk82.dim(", or ") + paint("accent", "/onboard") + chalk82.dim(" to calibrate.")
39045
+ " " + chalk82.dim("No profile yet \u2014 ask a question, type ") + chalk82.cyan("use demo data") + chalk82.dim(", or ") + paint("accent", "/onboard") + chalk82.dim(" (progressive: profile \u2192 research \u2192 demo \u2192 your data).")
38486
39046
  );
38487
39047
  console.log();
38488
39048
  }
@@ -38564,6 +39124,8 @@ async function loadFirstRunDemo(ctx, scenario) {
38564
39124
  counts,
38565
39125
  ingested_at: (/* @__PURE__ */ new Date()).toISOString()
38566
39126
  };
39127
+ const { markDemoDataSeen: markDemoDataSeen2 } = await Promise.resolve().then(() => (init_onboard_tiers(), onboard_tiers_exports));
39128
+ markDemoDataSeen2();
38567
39129
  const { saveSessionState: saveSessionState2 } = await Promise.resolve().then(() => (init_context2(), context_exports));
38568
39130
  saveSessionState2(ctx);
38569
39131
  return true;
@@ -38927,7 +39489,8 @@ async function conversationRouter(input, ctx) {
38927
39489
  const phase = resolveConversationPhase(ctx);
38928
39490
  if (looksLikeFilePath(line)) {
38929
39491
  const path = extractFilePath(line);
38930
- await ingestFromChat(ctx, path);
39492
+ const { ingestPathFromChat: ingestPathFromChat2 } = await Promise.resolve().then(() => (init_ingest_chat(), ingest_chat_exports));
39493
+ await ingestPathFromChat2(ctx, path);
38931
39494
  return { handled: true, summary: "Data ingested" };
38932
39495
  }
38933
39496
  if (isDemoIntent(line)) {
@@ -39853,7 +40416,7 @@ __export(repl_exports, {
39853
40416
  import { createInterface as createInterface2 } from "readline/promises";
39854
40417
  import { clearLine as clearLine2, cursorTo as cursorTo2 } from "readline";
39855
40418
  import chalk88 from "chalk";
39856
- import { join as join38 } from "path";
40419
+ import { join as join39 } from "path";
39857
40420
  function buildPrompt(ctx) {
39858
40421
  return buildConversationPrompt(ctx);
39859
40422
  }
@@ -40232,7 +40795,7 @@ function printHelp() {
40232
40795
  ["/remember <fact>", "Store a fact, a decision, or a preference"],
40233
40796
  ["/recall [topic]", "Show what NTRP stores about your business"],
40234
40797
  ["/rate good|bad <note>", "Correct the last answer. A bad note becomes a calibration"],
40235
- [`${join38(ntrpHome(), ANALYST_FILE_NAME)}`, "Standing operator instructions (tone, priorities, house rules)"]
40798
+ [`${join39(ntrpHome(), ANALYST_FILE_NAME)}`, "Standing operator instructions (tone, priorities, house rules)"]
40236
40799
  ];
40237
40800
  const teachMaxW = Math.max(...teach.map(([c]) => c.length)) + 2;
40238
40801
  for (const [cmd, desc] of teach) {