@sonnechasser/ntrp 1.4.9 → 1.5.2

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 +793 -176
  2. package/dist/mcp/server.js +1137 -1001
  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
  );
@@ -9433,6 +9438,8 @@ var init_repl_globals = __esm({
9433
9438
  var prompts_exports = {};
9434
9439
  __export(prompts_exports, {
9435
9440
  createPromptSession: () => createPromptSession,
9441
+ formatConfirmDefaultHint: () => formatConfirmDefaultHint,
9442
+ formatEnterDefaultHint: () => formatEnterDefaultHint,
9436
9443
  presentRecommendedFirst: () => presentRecommendedFirst,
9437
9444
  presentRecommendedMultiFirst: () => presentRecommendedMultiFirst,
9438
9445
  resolveAskMultiInput: () => resolveAskMultiInput,
@@ -9453,17 +9460,29 @@ function secretPromptLine(question) {
9453
9460
  function stripTerminalArtifacts(input) {
9454
9461
  return input.replace(/\x1b\[[0-9;]*[a-zA-Z~]/g, "").replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, "").replace(/\x1b\[200~/g, "").replace(/\x1b\[201~/g, "");
9455
9462
  }
9463
+ function formatEnterDefaultHint(defaultValue) {
9464
+ const lower = defaultValue.trim().toLowerCase();
9465
+ if (lower === "y" || lower === "yes") return "\u23CE yes";
9466
+ if (lower === "n" || lower === "no") return "\u23CE no";
9467
+ return defaultValue;
9468
+ }
9469
+ function formatConfirmDefaultHint(defaultYes = true) {
9470
+ return formatEnterDefaultHint(defaultYes ? "yes" : "no");
9471
+ }
9456
9472
  function renderQuestion(question, defaultValue) {
9457
9473
  const base = ` ${marker()}${bold(question)}`;
9458
9474
  if (defaultValue !== void 0 && defaultValue !== "") {
9459
- return `${base} ${chalk4.dim(`[${defaultValue}]`)} `;
9475
+ return `${base} ${chalk4.dim(`[${formatEnterDefaultHint(defaultValue)}]`)} `;
9460
9476
  }
9461
9477
  return `${base} `;
9462
9478
  }
9463
9479
  function resolveConfirmInput(raw, defaultYes = true) {
9464
9480
  const answer = raw.trim().toLowerCase();
9465
9481
  if (!answer) return defaultYes;
9466
- return answer === "y" || answer === "yes";
9482
+ if (answer === "y" || answer === "yes" || answer === "yeah" || answer === "yep") {
9483
+ return true;
9484
+ }
9485
+ return false;
9467
9486
  }
9468
9487
  function presentRecommendedFirst(choices, recommended) {
9469
9488
  if (choices.length === 0) return [];
@@ -9538,8 +9557,7 @@ function createPromptSession(existing, ctx) {
9538
9557
  }
9539
9558
  }
9540
9559
  async function confirm(question, defaultYes = true) {
9541
- const hint = defaultYes ? "Y/n" : "y/N";
9542
- const raw = (await rl.question(renderQuestion(question, hint))).trim();
9560
+ const raw = (await rl.question(renderQuestion(question, defaultYes ? "yes" : "no"))).trim();
9543
9561
  assertNotGlobalReplCommand(raw);
9544
9562
  return resolveConfirmInput(raw, defaultYes);
9545
9563
  }
@@ -12341,6 +12359,8 @@ var init_guide_slides = __esm({
12341
12359
  lines: [
12342
12360
  `We designed the conversation so you don't need a slash to start. Type the question you actually need \u2014 "is our retention real for the board?" or "pipeline health" \u2014 and NTRP restates it as a scope card so you can confirm we're answering the right thing.`,
12343
12361
  "",
12362
+ "Same navigational toolkit everywhere: when yes is the default, bare Enter accepts it (\u23CE yes on scope and strategy cards; \u23CE yes on wizard confirms). Type something else \u2014 b/back, adjust, n, or a restated focus \u2014 for another path.",
12363
+ "",
12344
12364
  "Hit Enter by accident on \u23CE yes? Type b or back \u2014 that rewinds to the focus card. The same word leaves a strategy or think overlay. It does not unload demo data or undo compute; /scratch or a new session is the blunt reset.",
12345
12365
  "",
12346
12366
  "Empty dataset? Load a sample with \u23CE use demo data, paste a CSV path, or /ingest. When the gap card says the formulas can compute, \u23CE go ahead runs the local math.",
@@ -22822,9 +22842,9 @@ async function handleGetSessionBrief(input) {
22822
22842
  if (!target) {
22823
22843
  return { error: `No session matching "${raw}".` };
22824
22844
  }
22825
- const { existsSync: existsSync36, readFileSync: readFileSync24 } = await import("fs");
22845
+ const { existsSync: existsSync38, readFileSync: readFileSync24 } = await import("fs");
22826
22846
  const briefPath = contextDocPathForSession2(target.id);
22827
- if (!existsSync36(briefPath)) {
22847
+ if (!existsSync38(briefPath)) {
22828
22848
  return {
22829
22849
  session_id: target.id,
22830
22850
  error: "No context brief on disk for this session (created before brief storage existed).",
@@ -24277,20 +24297,128 @@ var init_diagnose = __esm({
24277
24297
  });
24278
24298
 
24279
24299
  // src/ai/profile-draft.ts
24280
- async function draftCompanyProfile(seed, _ctx) {
24300
+ async function draftCompanyProfile(seed, ctx, opts = {}) {
24281
24301
  if (!seed.company_url && !seed.company_name) {
24282
24302
  throw new Error("draftCompanyProfile requires a URL or a company name");
24283
24303
  }
24284
- seedOperatorIdentity(seed.company_name, seed.company_url);
24304
+ const wantResearch = opts.research === true || opts.research !== false && hasAnyLlmProvider();
24305
+ if (!wantResearch) {
24306
+ seedOperatorIdentity(seed.company_name, seed.company_url);
24307
+ const out = {};
24308
+ if (seed.company_name?.trim()) out.company_name = seed.company_name.trim();
24309
+ if (seed.company_url) out.company_url = seed.company_url;
24310
+ return out;
24311
+ }
24312
+ if (!ctx) {
24313
+ throw new Error("draftCompanyProfile research requires a Context");
24314
+ }
24315
+ assertReplAi(ctx);
24316
+ const inputBlock = seed.company_url ? `Company website: ${seed.company_url}${seed.company_name ? `
24317
+ Company name (hint): ${seed.company_name}` : ""}` : `Company name: ${seed.company_name}`;
24318
+ const userMessage = `${inputBlock}
24319
+
24320
+ 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.`;
24321
+ const { text } = await llmCompleteText("onboard", SYSTEM_PROMPT4, userMessage, 2048, ctx, {
24322
+ skipPseudonymize: true
24323
+ });
24324
+ const cleaned = stripFences3(text);
24325
+ let parsed;
24326
+ try {
24327
+ parsed = JSON.parse(cleaned);
24328
+ } catch {
24329
+ throw new Error("AI response is not valid JSON");
24330
+ }
24331
+ const draft = validateDraft(parsed);
24332
+ const nameForLexicon = draft.company_name ?? seed.company_name;
24333
+ seedOperatorIdentity(nameForLexicon, seed.company_url);
24334
+ if (seed.company_url && !draft.company_url) draft.company_url = seed.company_url;
24335
+ return draft;
24336
+ }
24337
+ function stripFences3(text) {
24338
+ const trimmed = text.trim();
24339
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
24340
+ if (fenced) return fenced[1].trim();
24341
+ return trimmed;
24342
+ }
24343
+ function validateDraft(raw) {
24285
24344
  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;
24345
+ const str2 = (k) => {
24346
+ const v = raw[k];
24347
+ return typeof v === "string" && v.trim().length > 0 ? v.trim() : void 0;
24348
+ };
24349
+ const companyName = str2("company_name");
24350
+ if (companyName) out.company_name = companyName;
24351
+ const industry = str2("industry");
24352
+ if (industry) out.industry = industry;
24353
+ const productDescription = str2("product_description");
24354
+ if (productDescription) out.product_description = productDescription;
24355
+ const targetCustomer = str2("target_customer");
24356
+ if (targetCustomer) out.target_customer = targetCustomer;
24357
+ const motion = str2("sales_motion")?.toLowerCase();
24358
+ if (motion && ALLOWED_MOTIONS.has(motion)) {
24359
+ out.sales_motion = motion;
24360
+ }
24361
+ const dealSize = str2("average_deal_size");
24362
+ if (dealSize) out.average_deal_size = dealSize;
24363
+ const cycleDays = raw["sales_cycle_days"];
24364
+ if (typeof cycleDays === "number" && Number.isFinite(cycleDays) && cycleDays > 0) {
24365
+ out.sales_cycle_days = Math.round(cycleDays);
24366
+ }
24367
+ const crm = str2("primary_crm")?.toLowerCase();
24368
+ if (crm && ALLOWED_CRMS.has(crm)) out.primary_crm = crm;
24369
+ const engagement = str2("engagement_tool")?.toLowerCase();
24370
+ if (engagement && ALLOWED_ENGAGEMENT.has(engagement)) out.engagement_tool = engagement;
24288
24371
  return out;
24289
24372
  }
24373
+ var SYSTEM_PROMPT4, ALLOWED_MOTIONS, ALLOWED_CRMS, ALLOWED_ENGAGEMENT;
24290
24374
  var init_profile_draft = __esm({
24291
24375
  "src/ai/profile-draft.ts"() {
24292
24376
  "use strict";
24377
+ init_repl_api();
24378
+ init_complete();
24293
24379
  init_lexicon_seed();
24380
+ 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.
24381
+
24382
+ RESEARCH DEPTH \u2014 this is the most important part:
24383
+ - 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.
24384
+ - 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.
24385
+ - 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)?
24386
+ - Identify the most probable sales motion. Your guess becomes the wizard's default.
24387
+ - 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.
24388
+ - 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.).
24389
+
24390
+ FIELDS \u2014 emit only the ones you're reasonably confident about; the wizard will ask the user interactively for anything you omit.
24391
+
24392
+ REQUIRED (emit these whenever possible):
24393
+ - company_name: string \u2014 the canonical marketed name of the company (e.g. "HashiCorp", not "hashicorp.com")
24394
+ - 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")
24395
+ - 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.)
24396
+ - target_customer: string \u2014 plain-English ICP: company size, geography, persona titles, deal triggers. Be specific.
24397
+ - sales_motion: "plg" | "smb_velocity" | "mid_market" | "enterprise" \u2014 the motion most consistent with the product + ICP
24398
+
24399
+ OPTIONAL (include only with reasonable confidence):
24400
+ - average_deal_size: string \u2014 a range like "$5K-$50K ACV", "$45K-$120K (hardware + year 1 software)", "$250K+"
24401
+ - sales_cycle_days: integer \u2014 median cycle length in days
24402
+ - primary_crm: "salesforce" | "hubspot" | "pipedrive" | "other" \u2014 only with a signal (brand tier, pricing, known choice)
24403
+ - engagement_tool: "outreach" | "salesloft" | "apollo" | "none" \u2014 same bar
24404
+
24405
+ OUTPUT FORMAT \u2014 respond with STRICT JSON only, no preamble, no markdown fences, no commentary:
24406
+ {
24407
+ "company_name": "string",
24408
+ "industry": "string",
24409
+ "product_description": "string",
24410
+ "target_customer": "string",
24411
+ "sales_motion": "plg" | "smb_velocity" | "mid_market" | "enterprise",
24412
+ "average_deal_size": "string",
24413
+ "sales_cycle_days": number,
24414
+ "primary_crm": "salesforce" | "hubspot" | "pipedrive" | "other",
24415
+ "engagement_tool": "outreach" | "salesloft" | "apollo" | "none"
24416
+ }
24417
+
24418
+ 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.`;
24419
+ ALLOWED_MOTIONS = /* @__PURE__ */ new Set(["plg", "smb_velocity", "mid_market", "enterprise"]);
24420
+ ALLOWED_CRMS = /* @__PURE__ */ new Set(["salesforce", "hubspot", "pipedrive", "other"]);
24421
+ ALLOWED_ENGAGEMENT = /* @__PURE__ */ new Set(["outreach", "salesloft", "apollo", "none"]);
24294
24422
  }
24295
24423
  });
24296
24424
 
@@ -24532,7 +24660,7 @@ OUTPUT \u2014 STRICT JSON only, no fences, no commentary:
24532
24660
  });
24533
24661
 
24534
24662
  // src/ai/profile-clarify.ts
24535
- function stripFences3(text) {
24663
+ function stripFences4(text) {
24536
24664
  const trimmed = text.trim();
24537
24665
  const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
24538
24666
  if (fenced) return fenced[1].trim();
@@ -24552,7 +24680,7 @@ function formatDraft(draft) {
24552
24680
  return parts.join("\n");
24553
24681
  }
24554
24682
  function parseClarifyingQuestions(text) {
24555
- const cleaned = stripFences3(text);
24683
+ const cleaned = stripFences4(text);
24556
24684
  let parsed;
24557
24685
  try {
24558
24686
  parsed = JSON.parse(cleaned);
@@ -24649,7 +24777,7 @@ ${draftBlock}${userBlock}${answersBlock}
24649
24777
 
24650
24778
  Emit the refined profile now as STRICT JSON.`;
24651
24779
  const { text } = await llmCompleteText("onboard", REFINE_SYSTEM_PROMPT, userMessage, 2048, ctx);
24652
- const cleaned = stripFences3(text);
24780
+ const cleaned = stripFences4(text);
24653
24781
  let parsed;
24654
24782
  try {
24655
24783
  parsed = JSON.parse(cleaned);
@@ -24671,7 +24799,7 @@ function validateRefined(raw) {
24671
24799
  const targetCustomer = str2("target_customer");
24672
24800
  if (targetCustomer) out.target_customer = targetCustomer;
24673
24801
  const motion = str2("sales_motion")?.toLowerCase();
24674
- if (motion && ALLOWED_MOTIONS.has(motion)) {
24802
+ if (motion && ALLOWED_MOTIONS2.has(motion)) {
24675
24803
  out.sales_motion = motion;
24676
24804
  }
24677
24805
  const dealSize = str2("average_deal_size");
@@ -24681,14 +24809,14 @@ function validateRefined(raw) {
24681
24809
  out.sales_cycle_days = Math.round(cycleDays);
24682
24810
  }
24683
24811
  const crm = str2("primary_crm")?.toLowerCase();
24684
- if (crm && ALLOWED_CRMS.has(crm)) out.primary_crm = crm;
24812
+ if (crm && ALLOWED_CRMS2.has(crm)) out.primary_crm = crm;
24685
24813
  const engagement = str2("engagement_tool")?.toLowerCase();
24686
- if (engagement && ALLOWED_ENGAGEMENT.has(engagement)) out.engagement_tool = engagement;
24814
+ if (engagement && ALLOWED_ENGAGEMENT2.has(engagement)) out.engagement_tool = engagement;
24687
24815
  const userScope = str2("user_scope");
24688
24816
  if (userScope) out.user_scope = userScope;
24689
24817
  return out;
24690
24818
  }
24691
- var CLARIFY_SYSTEM_PROMPT, REFINE_SYSTEM_PROMPT, ALLOWED_MOTIONS, ALLOWED_CRMS, ALLOWED_ENGAGEMENT;
24819
+ var CLARIFY_SYSTEM_PROMPT, REFINE_SYSTEM_PROMPT, ALLOWED_MOTIONS2, ALLOWED_CRMS2, ALLOWED_ENGAGEMENT2;
24692
24820
  var init_profile_clarify = __esm({
24693
24821
  "src/ai/profile-clarify.ts"() {
24694
24822
  "use strict";
@@ -24753,9 +24881,9 @@ OUTPUT FORMAT \u2014 respond with STRICT JSON only, no preamble, no markdown fen
24753
24881
  "engagement_tool": "outreach",
24754
24882
  "user_scope": "string"
24755
24883
  }`;
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"]);
24884
+ ALLOWED_MOTIONS2 = /* @__PURE__ */ new Set(["plg", "smb_velocity", "mid_market", "enterprise"]);
24885
+ ALLOWED_CRMS2 = /* @__PURE__ */ new Set(["salesforce", "hubspot", "pipedrive", "other"]);
24886
+ ALLOWED_ENGAGEMENT2 = /* @__PURE__ */ new Set(["outreach", "salesloft", "apollo", "none"]);
24759
24887
  }
24760
24888
  });
24761
24889
 
@@ -25014,6 +25142,155 @@ var init_profile2 = __esm({
25014
25142
  }
25015
25143
  });
25016
25144
 
25145
+ // src/conversation/onboard-tiers.ts
25146
+ var onboard_tiers_exports = {};
25147
+ __export(onboard_tiers_exports, {
25148
+ ONBOARD_TIER_ORDER: () => ONBOARD_TIER_ORDER,
25149
+ canRunDomainTier: () => canRunDomainTier,
25150
+ clearOnboardTierFlags: () => clearOnboardTierFlags,
25151
+ describeOnboardTier: () => describeOnboardTier,
25152
+ getOnboardTierFlag: () => getOnboardTierFlag,
25153
+ getOnboardTierStatus: () => getOnboardTierStatus,
25154
+ listCompletedOnboardTier: () => listCompletedOnboardTier,
25155
+ markDemoDataSeen: () => markDemoDataSeen,
25156
+ markOnboardTierComplete: () => markOnboardTierComplete,
25157
+ markProductionDataSeen: () => markProductionDataSeen,
25158
+ pathLooksPresent: () => pathLooksPresent,
25159
+ resetOnboardTierProgress: () => resetOnboardTierProgress,
25160
+ resolveNextOnboardTier: () => resolveNextOnboardTier
25161
+ });
25162
+ import { existsSync as existsSync25, statSync as statSync4 } from "fs";
25163
+ function flagSet(tier) {
25164
+ return Boolean(getConfigValue(TIER_CONFIG_KEYS[tier]));
25165
+ }
25166
+ function getOnboardTierFlag(tier) {
25167
+ return getConfigValue(TIER_CONFIG_KEYS[tier]);
25168
+ }
25169
+ function markOnboardTierComplete(...tiers) {
25170
+ const at = (/* @__PURE__ */ new Date()).toISOString();
25171
+ for (const tier of tiers) {
25172
+ if (!flagSet(tier)) setConfigValue(TIER_CONFIG_KEYS[tier], at);
25173
+ }
25174
+ }
25175
+ function clearOnboardTierFlags(...tiers) {
25176
+ for (const tier of tiers) {
25177
+ deleteConfigValue(TIER_CONFIG_KEYS[tier]);
25178
+ }
25179
+ }
25180
+ function resetOnboardTierProgress() {
25181
+ clearOnboardTierFlags(...ONBOARD_TIER_ORDER);
25182
+ }
25183
+ function hasProductionDataset(ctx) {
25184
+ const source = ctx?.dataset?.source;
25185
+ if (source && !source.startsWith("demo:")) {
25186
+ if (source.startsWith("/") || /^[A-Za-z]:[\\/]/.test(source) || source.startsWith("~")) {
25187
+ return true;
25188
+ }
25189
+ if (!source.includes(":") && existsSync25(source)) return true;
25190
+ if (source.startsWith("csv:") || source.startsWith("file:") || source.startsWith("folder:")) {
25191
+ return true;
25192
+ }
25193
+ }
25194
+ if (ctx?.attachments && ctx.attachments.length > 0) return true;
25195
+ return flagSet("production");
25196
+ }
25197
+ function hasDemoExperience(ctx) {
25198
+ if (flagSet("demo")) return true;
25199
+ if (getPreferredDemoScenario()) return true;
25200
+ const source = ctx?.dataset?.source;
25201
+ if (source?.startsWith("demo:")) return true;
25202
+ return false;
25203
+ }
25204
+ function listCompletedOnboardTier(ctx) {
25205
+ const done = [];
25206
+ const profileOk = flagSet("profile") || isProfileConfigured(loadProfile());
25207
+ if (profileOk) done.push("profile");
25208
+ else return done;
25209
+ if (flagSet("domain")) done.push("domain");
25210
+ else return done;
25211
+ if (hasDemoExperience(ctx)) done.push("demo");
25212
+ else return done;
25213
+ if (hasProductionDataset(ctx) || flagSet("production")) done.push("production");
25214
+ return done;
25215
+ }
25216
+ function resolveNextOnboardTier(ctx) {
25217
+ const done = new Set(listCompletedOnboardTier(ctx));
25218
+ for (const tier of ONBOARD_TIER_ORDER) {
25219
+ if (!done.has(tier)) return tier;
25220
+ }
25221
+ return null;
25222
+ }
25223
+ function getOnboardTierStatus(ctx) {
25224
+ const completed = listCompletedOnboardTier(ctx);
25225
+ const next = resolveNextOnboardTier(ctx);
25226
+ const meta = next ? TIER_META[next] : null;
25227
+ return {
25228
+ completed,
25229
+ next,
25230
+ nextLabel: meta?.label ?? null,
25231
+ nextHint: meta?.hint ?? null
25232
+ };
25233
+ }
25234
+ function describeOnboardTier(tier) {
25235
+ return TIER_META[tier];
25236
+ }
25237
+ function canRunDomainTier() {
25238
+ return isProfileConfigured(loadProfile()) && hasAnyLlmProvider();
25239
+ }
25240
+ function markProductionDataSeen() {
25241
+ markOnboardTierComplete("production");
25242
+ }
25243
+ function markDemoDataSeen() {
25244
+ markOnboardTierComplete("demo");
25245
+ }
25246
+ function pathLooksPresent(raw) {
25247
+ try {
25248
+ return existsSync25(raw) && (statSync4(raw).isFile() || statSync4(raw).isDirectory());
25249
+ } catch {
25250
+ return false;
25251
+ }
25252
+ }
25253
+ var ONBOARD_TIER_ORDER, TIER_CONFIG_KEYS, TIER_META;
25254
+ var init_onboard_tiers = __esm({
25255
+ "src/conversation/onboard-tiers.ts"() {
25256
+ "use strict";
25257
+ init_store();
25258
+ init_profile();
25259
+ init_repl_api();
25260
+ init_scenario_fit();
25261
+ ONBOARD_TIER_ORDER = [
25262
+ "profile",
25263
+ "domain",
25264
+ "demo",
25265
+ "production"
25266
+ ];
25267
+ TIER_CONFIG_KEYS = {
25268
+ profile: "onboard-tier-profile",
25269
+ domain: "onboard-tier-domain",
25270
+ demo: "onboard-tier-demo",
25271
+ production: "onboard-tier-production"
25272
+ };
25273
+ TIER_META = {
25274
+ profile: {
25275
+ label: "Company profile",
25276
+ hint: "Name, industry, ICP \u2014 works without an API key"
25277
+ },
25278
+ domain: {
25279
+ label: "Domain research",
25280
+ hint: "Connect a key and let NTRP research your company"
25281
+ },
25282
+ demo: {
25283
+ label: "Sample data",
25284
+ hint: "Load a fitted demo book of business"
25285
+ },
25286
+ production: {
25287
+ label: "Your data",
25288
+ hint: "Drag-drop a CSV or folder path into the REPL"
25289
+ }
25290
+ };
25291
+ }
25292
+ });
25293
+
25017
25294
  // src/conversation/voice-setup.ts
25018
25295
  var voice_setup_exports = {};
25019
25296
  __export(voice_setup_exports, {
@@ -25373,97 +25650,318 @@ __export(onboard_exports, {
25373
25650
  profileExists: () => profileExists
25374
25651
  });
25375
25652
  import chalk25 from "chalk";
25653
+ import { existsSync as existsSync26 } from "fs";
25654
+ import { basename as basename7 } from "path";
25376
25655
  async function handler5(args, ctx) {
25377
25656
  const { flags } = parseArgs2(args, ["force", "skip-brand"]);
25378
25657
  const force = getBool(flags, "force");
25379
25658
  const skipBrand = getBool(flags, "skip-brand");
25659
+ const tierOverride = normalizeTierFlag(getString(flags, "tier"));
25380
25660
  const priorProfile = loadProfile();
25381
25661
  const configured = isProfileConfigured(priorProfile);
25382
25662
  let existing = configured ? priorProfile : null;
25383
25663
  if (!skipBrand) printCenteredLogo();
25384
25664
  const session = createPromptSession(ctx.rl, ctx);
25385
25665
  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;
25666
+ if (configured && !force && !tierOverride) {
25667
+ const status = getOnboardTierStatus(ctx);
25668
+ if (status.next === null) {
25669
+ console.log();
25670
+ console.log(" " + bold(`You're fully set up for ${priorProfile.company_name}.`));
25671
+ console.log(" " + chalk25.dim("Profile \xB7 domain research \xB7 sample data \xB7 production path \u2014 done."));
25672
+ console.log(
25673
+ " " + chalk25.dim("Type ") + paint("accent", "/onboard --force") + chalk25.dim(" to rebuild the profile, or drop a new CSV anytime.")
25674
+ );
25675
+ console.log();
25676
+ return `Onboarding complete for ${priorProfile.company_name}`;
25677
+ }
25678
+ if (status.next === "profile") {
25679
+ console.log();
25680
+ console.log(" " + bold(`Profile already exists for ${priorProfile.company_name}.`));
25681
+ const overwrite = await session.confirm("Overwrite this profile?", false);
25682
+ if (!overwrite) {
25683
+ console.log(" " + chalk25.dim("Keeping existing profile."));
25684
+ return;
25685
+ }
25686
+ existing = null;
25687
+ resetOnboardTierProgress();
25394
25688
  }
25689
+ } else if (configured && force && !tierOverride) {
25690
+ console.log();
25691
+ console.log(" " + bold(`Rebuilding profile for ${priorProfile.company_name}.`));
25395
25692
  existing = null;
25693
+ resetOnboardTierProgress();
25396
25694
  } else if (priorProfile && !configured) {
25397
25695
  console.log();
25398
- console.log(" " + chalk25.dim("NTRP found an incomplete profile. Setup continues."));
25696
+ console.log(" " + chalk25.dim("NTRP found an incomplete profile. Setup continues from the start."));
25399
25697
  existing = null;
25698
+ resetOnboardTierProgress();
25400
25699
  }
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.`));
25700
+ const next = tierOverride ?? (force ? "profile" : resolveNextOnboardTier(ctx));
25701
+ if (!next) {
25702
+ console.log();
25703
+ console.log(" " + bold("Onboarding is complete."));
25704
+ console.log(" " + chalk25.dim("Drop a CSV or folder path into the REPL anytime to refresh data."));
25705
+ console.log();
25706
+ return "Onboarding complete";
25707
+ }
25708
+ printTierIntro(next);
25709
+ switch (next) {
25710
+ case "profile":
25711
+ return await runProfileTier(session, ctx, existing);
25712
+ case "domain":
25713
+ return await runDomainTier(session, ctx);
25714
+ case "demo":
25715
+ return await runDemoTier(session, ctx);
25716
+ case "production":
25717
+ return await runProductionTier(session, ctx);
25450
25718
  }
25451
- saveProfile(candidate);
25452
- setConfigValue("sales-motion", candidate.sales_motion);
25453
- invalidateTaxonomy();
25454
- creditOnboardComplete(ctx);
25719
+ } finally {
25720
+ session.close();
25721
+ }
25722
+ }
25723
+ function normalizeTierFlag(raw) {
25724
+ if (!raw) return null;
25725
+ const t = raw.trim().toLowerCase();
25726
+ if (t === "profile" || t === "domain" || t === "demo" || t === "production") return t;
25727
+ return null;
25728
+ }
25729
+ function printTierIntro(tier) {
25730
+ const meta = describeOnboardTier(tier);
25731
+ console.log();
25732
+ console.log(" " + bold(`Onboard \xB7 ${meta.label}`));
25733
+ console.log(" " + chalk25.dim(meta.hint));
25734
+ console.log();
25735
+ }
25736
+ function printNextTierHint(ctx) {
25737
+ const status = getOnboardTierStatus(ctx);
25738
+ if (!status.next) {
25739
+ console.log(" " + chalk25.dim("You're through the progressive setup ladder."));
25740
+ return;
25741
+ }
25742
+ console.log(
25743
+ " " + chalk25.dim("Next: type ") + paint("accent", "/onboard") + chalk25.dim(` for ${status.nextLabel?.toLowerCase()} \u2014 ${status.nextHint}`)
25744
+ );
25745
+ }
25746
+ async function runProfileTier(session, ctx, existing) {
25747
+ console.log(" " + chalk25.dim("No API key needed for this step. Connect one later for domain research."));
25748
+ console.log();
25749
+ const siteOrName = await session.askRequired(
25750
+ "What is the company website? Or type the company name."
25751
+ );
25752
+ const isUrlish = looksLikeUrl(siteOrName);
25753
+ const companyUrl = isUrlish ? normalizeUrl(siteOrName) : void 0;
25754
+ const seedName = isUrlish ? void 0 : siteOrName.trim();
25755
+ const seed = {
25756
+ ...companyUrl ? { company_url: companyUrl } : {},
25757
+ ...seedName ? { company_name: seedName } : {}
25758
+ };
25759
+ let draft = await draftCompanyProfile(seed, ctx, { research: false });
25760
+ draft = await fillMissingFields(session, draft, existing);
25761
+ const canonicalName = draft.company_name ?? seedName ?? existing?.company_name ?? "";
25762
+ const salesMotion = draft.sales_motion ?? existing?.sales_motion ?? "mid_market";
25763
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
25764
+ let candidate = {
25765
+ schema_version: 1,
25766
+ company_name: canonicalName,
25767
+ ...companyUrl ? { company_url: companyUrl } : existing?.company_url ? { company_url: existing.company_url } : {},
25768
+ industry: draft.industry ?? existing?.industry ?? "",
25769
+ product_description: draft.product_description ?? existing?.product_description ?? "",
25770
+ target_customer: draft.target_customer ?? existing?.target_customer ?? "",
25771
+ sales_motion: salesMotion,
25772
+ ...draft.average_deal_size !== void 0 ? { average_deal_size: draft.average_deal_size } : {},
25773
+ ...draft.sales_cycle_days !== void 0 ? { sales_cycle_days: draft.sales_cycle_days } : {},
25774
+ ...draft.primary_crm !== void 0 ? { primary_crm: draft.primary_crm } : {},
25775
+ ...draft.engagement_tool !== void 0 ? { engagement_tool: draft.engagement_tool } : {},
25776
+ ...existing?.user_scope ? { user_scope: existing.user_scope } : {},
25777
+ created_at: existing?.created_at ?? now2,
25778
+ updated_at: now2
25779
+ };
25780
+ candidate = await reviewLoop(session, candidate);
25781
+ saveProfile(candidate);
25782
+ setConfigValue("sales-motion", candidate.sales_motion);
25783
+ invalidateTaxonomy();
25784
+ creditOnboardComplete(ctx);
25785
+ markOnboardTierComplete("profile");
25786
+ if (hasAnyLlmProvider()) {
25455
25787
  console.log();
25456
25788
  console.log(" " + chalk25.green("\u2713") + " " + bold(`Profile saved for ${candidate.company_name}`));
25457
- console.log(" " + chalk25.dim(`~/.ntrp/profile.json`));
25789
+ console.log(" " + chalk25.dim("A key is already connected \u2014 running domain research now."));
25458
25790
  console.log();
25791
+ await runDomainResearchOnProfile(session, ctx, candidate);
25792
+ markOnboardTierComplete("domain");
25459
25793
  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();
25794
+ const { offerVoiceSetup: offerVoiceSetup3 } = await Promise.resolve().then(() => (init_voice_setup(), voice_setup_exports));
25795
+ await offerVoiceSetup3(ctx, session);
25796
+ printNextTierHint(ctx);
25797
+ return `Profile + domain research saved for ${candidate.company_name}`;
25798
+ }
25799
+ console.log();
25800
+ console.log(" " + chalk25.green("\u2713") + " " + bold(`Profile saved for ${candidate.company_name}`));
25801
+ console.log(" " + chalk25.dim(`~/.ntrp/profile.json`));
25802
+ console.log();
25803
+ await offerInboxSkillSetup(session, { beat: "production" });
25804
+ const { offerVoiceSetup: offerVoiceSetup2 } = await Promise.resolve().then(() => (init_voice_setup(), voice_setup_exports));
25805
+ await offerVoiceSetup2(ctx, session);
25806
+ printNextTierHint(ctx);
25807
+ return `Profile saved for ${candidate.company_name}`;
25808
+ }
25809
+ async function runDomainTier(session, ctx) {
25810
+ const profile = loadProfile();
25811
+ if (!isProfileConfigured(profile) || !profile) {
25812
+ console.log(" " + chalk25.dim("Need a company profile first \u2014 starting there."));
25813
+ return runProfileTier(session, ctx, null);
25814
+ }
25815
+ await ensureLlmKeys(session);
25816
+ if (!hasAnyLlmProvider()) {
25817
+ console.log();
25818
+ console.log(" " + chalk25.dim("No key connected \u2014 domain research stays locked."));
25819
+ console.log(
25820
+ " " + chalk25.dim("Type ") + paint("accent", "/connect") + chalk25.dim(" then ") + paint("accent", "/onboard") + chalk25.dim(" to research ") + bold(profile.company_name) + chalk25.dim(".")
25821
+ );
25822
+ console.log();
25823
+ return "Domain research skipped \u2014 no API key";
25824
+ }
25825
+ const updated = await runDomainResearchOnProfile(session, ctx, profile);
25826
+ markOnboardTierComplete("domain");
25827
+ printNextTierHint(ctx);
25828
+ return `Domain research saved for ${updated.company_name}`;
25829
+ }
25830
+ async function runDomainResearchOnProfile(session, ctx, profile) {
25831
+ const seed = {
25832
+ ...profile.company_url ? { company_url: profile.company_url } : {},
25833
+ ...profile.company_name ? { company_name: profile.company_name } : {}
25834
+ };
25835
+ let draft = { ...profile };
25836
+ const spinner = makeSpinner("Researching your company\u2026");
25837
+ try {
25838
+ draft = await draftCompanyProfile(seed, ctx, { research: true });
25839
+ spinner.succeed("Research draft ready");
25840
+ } catch (err) {
25841
+ spinner.fail("Could not research the company");
25842
+ console.log(" " + chalk25.dim(String(err.message ?? err)));
25843
+ console.log(" " + chalk25.dim("Falling back to your existing profile \u2014 you can edit fields below."));
25844
+ draft = { ...profile };
25845
+ }
25846
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
25847
+ let candidate = {
25848
+ ...profile,
25849
+ company_name: draft.company_name ?? profile.company_name,
25850
+ ...draft.company_url || profile.company_url ? { company_url: draft.company_url ?? profile.company_url } : {},
25851
+ industry: draft.industry ?? profile.industry,
25852
+ product_description: draft.product_description ?? profile.product_description,
25853
+ target_customer: draft.target_customer ?? profile.target_customer,
25854
+ sales_motion: draft.sales_motion ?? profile.sales_motion,
25855
+ updated_at: now2
25856
+ };
25857
+ if (draft.average_deal_size !== void 0) candidate.average_deal_size = draft.average_deal_size;
25858
+ else if (profile.average_deal_size) candidate.average_deal_size = profile.average_deal_size;
25859
+ if (draft.sales_cycle_days !== void 0) candidate.sales_cycle_days = draft.sales_cycle_days;
25860
+ else if (profile.sales_cycle_days !== void 0) candidate.sales_cycle_days = profile.sales_cycle_days;
25861
+ if (draft.primary_crm !== void 0) candidate.primary_crm = draft.primary_crm;
25862
+ else if (profile.primary_crm) candidate.primary_crm = profile.primary_crm;
25863
+ if (draft.engagement_tool !== void 0) candidate.engagement_tool = draft.engagement_tool;
25864
+ else if (profile.engagement_tool) candidate.engagement_tool = profile.engagement_tool;
25865
+ const filled = await fillMissingFields(session, candidate, profile);
25866
+ candidate = {
25867
+ ...candidate,
25868
+ company_name: filled.company_name ?? candidate.company_name,
25869
+ industry: filled.industry ?? candidate.industry,
25870
+ product_description: filled.product_description ?? candidate.product_description,
25871
+ target_customer: filled.target_customer ?? candidate.target_customer,
25872
+ ...filled.sales_motion ? { sales_motion: filled.sales_motion } : {},
25873
+ ...filled.average_deal_size !== void 0 ? { average_deal_size: filled.average_deal_size } : {},
25874
+ ...filled.sales_cycle_days !== void 0 ? { sales_cycle_days: filled.sales_cycle_days } : {},
25875
+ ...filled.primary_crm !== void 0 ? { primary_crm: filled.primary_crm } : {},
25876
+ ...filled.engagement_tool !== void 0 ? { engagement_tool: filled.engagement_tool } : {},
25877
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
25878
+ };
25879
+ renderProfile(candidate);
25880
+ candidate = await adaptiveRefine(session, candidate, ctx);
25881
+ candidate = await reviewLoop(session, candidate);
25882
+ saveProfile(candidate);
25883
+ setConfigValue("sales-motion", candidate.sales_motion);
25884
+ invalidateTaxonomy();
25885
+ console.log();
25886
+ console.log(" " + chalk25.green("\u2713") + " " + bold(`Domain research saved for ${candidate.company_name}`));
25887
+ console.log(" " + chalk25.dim(`~/.ntrp/profile.json`));
25888
+ console.log();
25889
+ return candidate;
25890
+ }
25891
+ async function runDemoTier(session, ctx) {
25892
+ const profile = loadProfile();
25893
+ if (!isProfileConfigured(profile) || !profile) {
25894
+ console.log(" " + chalk25.dim("Need a company profile first \u2014 starting there."));
25895
+ return runProfileTier(session, ctx, null);
25896
+ }
25897
+ console.log(" " + chalk25.dim("Fit a sample book of business shaped like yours \u2014 or skip to bring your own later."));
25898
+ console.log();
25899
+ const loaded2 = await offerFittedDemoAfterOnboard(session, ctx, profile);
25900
+ markDemoDataSeen();
25901
+ if (loaded2) {
25902
+ console.log();
25903
+ printNextTierHint(ctx);
25904
+ return "Demo data loaded";
25905
+ }
25906
+ console.log();
25907
+ console.log(" " + chalk25.dim("Skipped sample data. Next up is your production CSV or folder."));
25908
+ printNextTierHint(ctx);
25909
+ return "Demo data skipped";
25910
+ }
25911
+ async function runProductionTier(session, ctx) {
25912
+ console.log(" " + bold("Bring your own pipeline data"));
25913
+ console.log(" " + chalk25.dim("Drag a CSV (or a folder of CSVs) onto this terminal \u2014 same as dropping a path in a shell."));
25914
+ console.log(" " + chalk25.dim("NTRP will read the path and ingest what it can. Or type the path and press Enter."));
25915
+ console.log();
25916
+ const raw = await session.ask("CSV or folder path. Press Enter to skip for now", { default: "" });
25917
+ const trimmed = raw.trim().replace(/^["']|["']$/g, "");
25918
+ if (!trimmed) {
25919
+ markProductionDataSeen();
25920
+ console.log();
25921
+ console.log(
25922
+ " " + chalk25.dim("Skipped. Drop a file path into the REPL anytime \u2014 NTRP will pick it up.")
25923
+ );
25924
+ console.log();
25925
+ return "Production data skipped";
25926
+ }
25927
+ const resolved = resolveUserPath(trimmed);
25928
+ if (!existsSync26(resolved)) {
25929
+ console.log(" " + chalk25.red(`Path not found: ${resolved}`));
25930
+ console.log(" " + chalk25.dim("Try again with /onboard, or drop the path into the REPL."));
25931
+ return "Production path not found";
25932
+ }
25933
+ const { ingestPathFromChat: ingestPathFromChat2 } = await Promise.resolve().then(() => (init_ingest_chat(), ingest_chat_exports));
25934
+ const ok = await ingestPathFromChat2(ctx, resolved);
25935
+ if (ok) {
25936
+ markProductionDataSeen();
25937
+ printNextTierHint(ctx);
25938
+ return `Production data loaded from ${basename7(resolved)}`;
25939
+ }
25940
+ console.log(" " + chalk25.dim("Nothing ingested. Drop a CSV path into the REPL when ready."));
25941
+ return "Production ingest cancelled";
25942
+ }
25943
+ async function reviewLoop(session, initial) {
25944
+ let candidate = initial;
25945
+ for (; ; ) {
25946
+ renderProfile(candidate);
25947
+ const action = await session.choose(
25948
+ "Is this profile correct?",
25949
+ [
25950
+ { value: "yes", label: "Yes \u2014 save this profile" },
25951
+ { value: "edit", label: "Edit fields" },
25952
+ { value: "redraft", label: "Redraft from scratch" }
25953
+ ],
25954
+ { default: "yes" }
25955
+ );
25956
+ if (action === "yes") break;
25957
+ if (action === "redraft") {
25958
+ const fresh = await fillMissingFields(session, { company_url: candidate.company_url }, null);
25959
+ candidate = applyDraft(candidate, fresh);
25960
+ continue;
25961
+ }
25962
+ candidate = await editLoop(session, candidate);
25466
25963
  }
25964
+ return candidate;
25467
25965
  }
25468
25966
  async function fillMissingFields(session, draft, existing) {
25469
25967
  const out = { ...draft };
@@ -25685,16 +26183,10 @@ async function editField(session, p, field) {
25685
26183
  }
25686
26184
  }
25687
26185
  }
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
26186
  function printKeylessNote() {
25695
- console.log(" " + chalk25.dim("Skipping AI \u2014 I'll ask a few quick questions instead."));
26187
+ console.log(" " + chalk25.dim("Skipping AI \u2014 domain research needs a key."));
25696
26188
  console.log(
25697
- " " + chalk25.dim("Add a key anytime with ") + paint("accent", "/connect") + chalk25.dim(" to unlock research, findings, and chat answers.")
26189
+ " " + chalk25.dim("Add a key anytime with ") + paint("accent", "/connect") + chalk25.dim(" then type ") + paint("accent", "/onboard") + chalk25.dim(".")
25698
26190
  );
25699
26191
  console.log();
25700
26192
  }
@@ -25704,9 +26196,9 @@ async function ensureLlmKeys(session) {
25704
26196
  const { providerLabel: providerLabel2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
25705
26197
  const { countAvailableEngines: countAvailableEngines2 } = await Promise.resolve().then(() => (init_session_state(), session_state_exports));
25706
26198
  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."));
26199
+ console.log(" " + bold("Connect a key") + chalk25.dim(" (needed for domain research. Press Enter to skip)"));
26200
+ console.log(" " + chalk25.dim("Scores and metrics are local math. No AI is required for vitals."));
26201
+ console.log(" " + chalk25.dim("A key unlocks company research now, plus findings and chat answers later."));
25710
26202
  console.log(
25711
26203
  " " + chalk25.dim("Paste any provider API key. Anthropic, OpenAI, Groq, Gemini, Mistral, and others.")
25712
26204
  );
@@ -25798,6 +26290,8 @@ var init_onboard = __esm({
25798
26290
  init_profile_clarify();
25799
26291
  init_profile2();
25800
26292
  init_inbox_setup();
26293
+ init_onboard_tiers();
26294
+ init_path_safety();
25801
26295
  SALES_MOTION_CHOICES = [
25802
26296
  { value: "plg", label: "PLG \u2014 Product-Led Growth", description: PRESET_DESCRIPTIONS.plg },
25803
26297
  { value: "smb_velocity", label: "SMB Velocity \u2014 Fast cycles, high volume", description: PRESET_DESCRIPTIONS.smb_velocity },
@@ -25838,8 +26332,8 @@ __export(new_exports, {
25838
26332
  handler: () => handler6
25839
26333
  });
25840
26334
  import chalk26 from "chalk";
25841
- import { existsSync as existsSync25 } from "fs";
25842
- import { basename as basename7 } from "path";
26335
+ import { existsSync as existsSync27 } from "fs";
26336
+ import { basename as basename8 } from "path";
25843
26337
  async function handler6(args, ctx) {
25844
26338
  const { positional, flags } = parseArgs2(args, ["demo", "empty", "list-scenarios", "regen-taxonomy"]);
25845
26339
  let source = null;
@@ -25860,7 +26354,7 @@ async function handler6(args, ctx) {
25860
26354
  console.error(chalk26.red(" Usage: /new <file.csv> | --demo [--scenario <name>] | --empty [--lens health|metrics]"));
25861
26355
  return;
25862
26356
  }
25863
- if (source.kind === "file" && !existsSync25(source.path)) {
26357
+ if (source.kind === "file" && !existsSync27(source.path)) {
25864
26358
  console.error(chalk26.red(` File not found: ${source.path}`));
25865
26359
  return;
25866
26360
  }
@@ -25884,7 +26378,7 @@ async function handler6(args, ctx) {
25884
26378
  const sourceFlag = getString(flags, "source", "s");
25885
26379
  if (sourceFlag) passthrough.push("--source", sourceFlag);
25886
26380
  await ingest(passthrough, ctx);
25887
- datasetLabel2 = basename7(source.path);
26381
+ datasetLabel2 = basename8(source.path);
25888
26382
  datasetSource = source.path;
25889
26383
  } else if (source.kind === "demo") {
25890
26384
  const { handler: demo } = await Promise.resolve().then(() => (init_generate(), generate_exports));
@@ -26087,7 +26581,7 @@ __export(end_exports, {
26087
26581
  handler: () => handler7
26088
26582
  });
26089
26583
  import chalk27 from "chalk";
26090
- import { existsSync as existsSync26 } from "fs";
26584
+ import { existsSync as existsSync28 } from "fs";
26091
26585
  async function handler7(args, ctx) {
26092
26586
  if (args.length > 0) {
26093
26587
  console.error(chalk27.red(" Usage: /end"));
@@ -26124,10 +26618,10 @@ async function handler7(args, ctx) {
26124
26618
  if (summary) {
26125
26619
  console.log(" " + chalk27.dim(summary));
26126
26620
  }
26127
- if (existsSync26(transcriptPathForSession(endedId))) {
26621
+ if (existsSync28(transcriptPathForSession(endedId))) {
26128
26622
  console.log(" " + chalk27.dim("Transcript: ") + chalk27.dim(transcriptPathForSession(endedId)));
26129
26623
  }
26130
- if (existsSync26(contextDocPathForSession(endedId))) {
26624
+ if (existsSync28(contextDocPathForSession(endedId))) {
26131
26625
  console.log(" " + chalk27.dim("Context brief: ") + chalk27.dim(contextDocPathForSession(endedId)));
26132
26626
  }
26133
26627
  console.log();
@@ -26149,7 +26643,7 @@ __export(session_exports, {
26149
26643
  });
26150
26644
  import chalk28 from "chalk";
26151
26645
  import { join as join23 } from "path";
26152
- import { existsSync as existsSync27 } from "fs";
26646
+ import { existsSync as existsSync29 } from "fs";
26153
26647
  async function handler8(args, ctx) {
26154
26648
  const sub = args[0];
26155
26649
  if (!sub) return listSessionsView(ctx);
@@ -26285,7 +26779,7 @@ async function pickUp(idArg, ctx) {
26285
26779
  if (session.strategist && session.strategist.step !== "awaiting_analysis") {
26286
26780
  const objective = session.strategist.objective;
26287
26781
  console.log(
26288
- " " + chalk28.yellow("Resuming mid-strategy") + (objective ? chalk28.dim(`: "${objective}"`) : "") + chalk28.dim(" \u2014 say ") + chalk28.cyan("yes") + chalk28.dim(" to continue or ") + chalk28.cyan("cancel") + chalk28.dim(" to drop it.")
26782
+ " " + chalk28.yellow("Resuming mid-strategy") + (objective ? chalk28.dim(`: "${objective}"`) : "") + chalk28.dim(" \u2014 Confirm? ") + chalk28.cyan("\u23CE yes") + chalk28.dim(" \xB7 ") + chalk28.cyan("cancel") + chalk28.dim(" to drop it.")
26289
26783
  );
26290
26784
  }
26291
26785
  if (session.think && session.think.step === "active") {
@@ -26295,7 +26789,7 @@ async function pickUp(idArg, ctx) {
26295
26789
  );
26296
26790
  }
26297
26791
  const contextPath = contextDocPathForSession(target.id);
26298
- if (existsSync27(contextPath)) {
26792
+ if (existsSync29(contextPath)) {
26299
26793
  console.log(" " + chalk28.dim("Context brief: ") + chalk28.dim(contextPath));
26300
26794
  }
26301
26795
  console.log();
@@ -27247,7 +27741,7 @@ var init_bundle = __esm({
27247
27741
 
27248
27742
  // src/repositories/markdown.ts
27249
27743
  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";
27744
+ import { basename as basename9, dirname as dirname5, join as join26, resolve as resolve8 } from "path";
27251
27745
  import { stringify as stringifyYaml2 } from "yaml";
27252
27746
  function renderMarkdownFiles(pkg) {
27253
27747
  const bundleJson = JSON.stringify(pkg, null, 2) + "\n";
@@ -27437,7 +27931,7 @@ function getRootPath(target) {
27437
27931
  return resolve8(target.directory ?? `ntrp-repository-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
27438
27932
  }
27439
27933
  function safeFilename(value) {
27440
- return (basename8(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "strategy") + ".md";
27934
+ return (basename9(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "strategy") + ".md";
27441
27935
  }
27442
27936
  function escapeSummary(value) {
27443
27937
  return value.replace(/[<>]/g, "");
@@ -29974,7 +30468,7 @@ async function handleStrategizeFlow(input, ctx) {
29974
30468
  " " + chalk37.dim("That looks like a question. A strategy objective is waiting.")
29975
30469
  );
29976
30470
  console.log(
29977
- " " + chalk37.dim("Type ") + chalk37.cyan("yes") + chalk37.dim(" to make the plan. Type ") + chalk37.cyan("b") + chalk37.dim(" / ") + chalk37.cyan("adjust") + chalk37.dim(" to change the objective. Type ") + chalk37.cyan("cancel") + chalk37.dim(" to answer questions first.")
30471
+ " " + chalk37.dim("Confirm? ") + chalk37.cyan("\u23CE yes") + chalk37.dim(" \xB7 ") + chalk37.cyan("b back") + chalk37.dim(" / ") + chalk37.cyan("adjust") + chalk37.dim(" \xB7 ") + chalk37.cyan("cancel") + chalk37.dim(" to answer questions first.")
29978
30472
  );
29979
30473
  console.log();
29980
30474
  return "Awaiting confirm";
@@ -29987,7 +30481,7 @@ async function handleStrategizeFlow(input, ctx) {
29987
30481
  }
29988
30482
  console.log();
29989
30483
  console.log(
29990
- " " + chalk37.dim("Type ") + chalk37.cyan("yes") + chalk37.dim(" to make the plan. Type ") + chalk37.cyan("b") + chalk37.dim(" / ") + chalk37.cyan("adjust") + chalk37.dim(" to change the objective. Type ") + chalk37.cyan("cancel") + chalk37.dim(" to stop.")
30484
+ " " + chalk37.dim("Confirm? ") + chalk37.cyan("\u23CE yes") + chalk37.dim(" \xB7 ") + chalk37.cyan("b back") + chalk37.dim(" / ") + chalk37.cyan("adjust") + chalk37.dim(" \xB7 ") + chalk37.cyan("cancel")
29991
30485
  );
29992
30486
  console.log();
29993
30487
  return "Awaiting confirm";
@@ -31967,7 +32461,7 @@ var init_checkout = __esm({
31967
32461
  });
31968
32462
 
31969
32463
  // src/services/setup.ts
31970
- import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync20, writeFileSync as writeFileSync19 } from "fs";
32464
+ import { existsSync as existsSync30, mkdirSync as mkdirSync17, readFileSync as readFileSync20, writeFileSync as writeFileSync19 } from "fs";
31971
32465
  import { join as join29 } from "path";
31972
32466
  function setupCheck() {
31973
32467
  const home = ntrpHome();
@@ -33403,7 +33897,7 @@ var init_metrics = __esm({
33403
33897
  });
33404
33898
 
33405
33899
  // src/ai/feedback-apply.ts
33406
- function stripFences4(text) {
33900
+ function stripFences5(text) {
33407
33901
  const trimmed = text.trim();
33408
33902
  const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
33409
33903
  if (fenced) return fenced[1].trim();
@@ -33433,8 +33927,8 @@ OPERATOR FEEDBACK:
33433
33927
  "${feedbackText}"
33434
33928
 
33435
33929
  Apply the feedback now as STRICT JSON.`;
33436
- const { text } = await llmCompleteText("feedback", SYSTEM_PROMPT4, userMessage, 1024, ctx);
33437
- const cleaned = stripFences4(text);
33930
+ const { text } = await llmCompleteText("feedback", SYSTEM_PROMPT5, userMessage, 1024, ctx);
33931
+ const cleaned = stripFences5(text);
33438
33932
  let parsed;
33439
33933
  try {
33440
33934
  parsed = JSON.parse(cleaned);
@@ -33461,7 +33955,7 @@ function validatePatch(raw) {
33461
33955
  const targetCustomer = str2("target_customer");
33462
33956
  if (targetCustomer) out.target_customer = targetCustomer;
33463
33957
  const motion = str2("sales_motion")?.toLowerCase();
33464
- if (motion && ALLOWED_MOTIONS2.has(motion)) {
33958
+ if (motion && ALLOWED_MOTIONS3.has(motion)) {
33465
33959
  out.sales_motion = motion;
33466
33960
  }
33467
33961
  const dealSize = str2("average_deal_size");
@@ -33471,25 +33965,25 @@ function validatePatch(raw) {
33471
33965
  out.sales_cycle_days = Math.round(cycleDays);
33472
33966
  }
33473
33967
  const crm = str2("primary_crm")?.toLowerCase();
33474
- if (crm && ALLOWED_CRMS2.has(crm)) out.primary_crm = crm;
33968
+ if (crm && ALLOWED_CRMS3.has(crm)) out.primary_crm = crm;
33475
33969
  const engagement = str2("engagement_tool")?.toLowerCase();
33476
- if (engagement && ALLOWED_ENGAGEMENT2.has(engagement)) out.engagement_tool = engagement;
33970
+ if (engagement && ALLOWED_ENGAGEMENT3.has(engagement)) out.engagement_tool = engagement;
33477
33971
  const userScope = str2("user_scope");
33478
33972
  if (userScope) out.user_scope = userScope;
33479
33973
  const customContext = str2("custom_context");
33480
33974
  if (customContext) out.custom_context = customContext;
33481
33975
  return out;
33482
33976
  }
33483
- var ALLOWED_MOTIONS2, ALLOWED_CRMS2, ALLOWED_ENGAGEMENT2, SYSTEM_PROMPT4;
33977
+ var ALLOWED_MOTIONS3, ALLOWED_CRMS3, ALLOWED_ENGAGEMENT3, SYSTEM_PROMPT5;
33484
33978
  var init_feedback_apply = __esm({
33485
33979
  "src/ai/feedback-apply.ts"() {
33486
33980
  "use strict";
33487
33981
  init_repl_api();
33488
33982
  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.
33983
+ ALLOWED_MOTIONS3 = /* @__PURE__ */ new Set(["plg", "smb_velocity", "mid_market", "enterprise"]);
33984
+ ALLOWED_CRMS3 = /* @__PURE__ */ new Set(["salesforce", "hubspot", "pipedrive", "other"]);
33985
+ ALLOWED_ENGAGEMENT3 = /* @__PURE__ */ new Set(["outreach", "salesloft", "apollo", "none"]);
33986
+ 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
33987
 
33494
33988
  YOUR JOB:
33495
33989
  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 +34468,7 @@ __export(sessions_exports, {
33974
34468
  handler: () => handler35
33975
34469
  });
33976
34470
  import chalk62 from "chalk";
33977
- import { existsSync as existsSync29 } from "fs";
34471
+ import { existsSync as existsSync31 } from "fs";
33978
34472
  async function handler35(args, _ctx) {
33979
34473
  const sub = args[0] ?? "list";
33980
34474
  if (sub === "list" || !args[0]) {
@@ -34081,12 +34575,12 @@ function showSession(idArg) {
34081
34575
  console.log();
34082
34576
  const transcriptPath = transcriptPathForSession(session.id);
34083
34577
  const contextPath = contextDocPathForSession(session.id);
34084
- if (existsSync29(transcriptPath) || existsSync29(contextPath)) {
34578
+ if (existsSync31(transcriptPath) || existsSync31(contextPath)) {
34085
34579
  console.log(" " + chalk62.dim("\u2500".repeat(40)));
34086
- if (existsSync29(contextPath)) {
34580
+ if (existsSync31(contextPath)) {
34087
34581
  console.log(" " + chalk62.dim("Context brief: ") + chalk62.dim(contextPath));
34088
34582
  }
34089
- if (existsSync29(transcriptPath)) {
34583
+ if (existsSync31(transcriptPath)) {
34090
34584
  console.log(" " + chalk62.dim("Full transcript: ") + chalk62.dim(transcriptPath));
34091
34585
  }
34092
34586
  console.log();
@@ -34355,6 +34849,7 @@ var init_privacy_notice = __esm({
34355
34849
  "Direct identifiers (names, emails, domains, deal names) are replaced with local tokens before any LLM HTTP call.",
34356
34850
  "The mapping stays in ~/.ntrp/privacy/ on this machine. The CLI shows real names; the provider never does.",
34357
34851
  "This is pseudonymization, not anonymization \u2014 you can reverse it; the model cannot.",
34852
+ "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
34853
  "Computed scores and dollar aggregates still go to the provider you connected.",
34359
34854
  "If you turn on web retrieval, named-account queries are refused. Generic GTM terms may go to Tavily or Brave.",
34360
34855
  "A custom --base-url receives the same tokenized payload.",
@@ -35422,20 +35917,20 @@ var init_model = __esm({
35422
35917
  });
35423
35918
 
35424
35919
  // src/config/update-check.ts
35425
- import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync21, unlinkSync as unlinkSync5, writeFileSync as writeFileSync20 } from "fs";
35920
+ import { existsSync as existsSync32, mkdirSync as mkdirSync18, readFileSync as readFileSync21, unlinkSync as unlinkSync5, writeFileSync as writeFileSync20 } from "fs";
35426
35921
  import { join as join33 } from "path";
35427
35922
  function cachePath2() {
35428
35923
  return join33(ntrpHome(), "update-check.json");
35429
35924
  }
35430
35925
  function ensureDir7() {
35431
35926
  const dir = ntrpHome();
35432
- if (!existsSync30(dir)) {
35927
+ if (!existsSync32(dir)) {
35433
35928
  mkdirSync18(dir, { recursive: true });
35434
35929
  }
35435
35930
  }
35436
35931
  function loadUpdateCheckCache() {
35437
35932
  const path = cachePath2();
35438
- if (!existsSync30(path)) return null;
35933
+ if (!existsSync32(path)) return null;
35439
35934
  try {
35440
35935
  const parsed = JSON.parse(readFileSync21(path, "utf-8"));
35441
35936
  if (!parsed || typeof parsed !== "object" || typeof parsed.lastCheck !== "number" || typeof parsed.latestVersion !== "string") {
@@ -35456,7 +35951,7 @@ function isCacheFresh(cache2, ttlMs = CACHE_TTL_MS2) {
35456
35951
  }
35457
35952
  function invalidateUpdateCheckCache() {
35458
35953
  const path = cachePath2();
35459
- if (existsSync30(path)) {
35954
+ if (existsSync32(path)) {
35460
35955
  unlinkSync5(path);
35461
35956
  }
35462
35957
  }
@@ -35470,11 +35965,11 @@ var init_update_check = __esm({
35470
35965
  });
35471
35966
 
35472
35967
  // src/version.ts
35473
- import { existsSync as existsSync31, readFileSync as readFileSync22 } from "fs";
35968
+ import { existsSync as existsSync33, readFileSync as readFileSync22 } from "fs";
35474
35969
  import { dirname as dirname6, join as join34 } from "path";
35475
35970
  import { fileURLToPath } from "url";
35476
35971
  function readVersionFromPackageJson(packageJsonPath) {
35477
- if (!existsSync31(packageJsonPath)) return null;
35972
+ if (!existsSync33(packageJsonPath)) return null;
35478
35973
  try {
35479
35974
  const pkg = JSON.parse(readFileSync22(packageJsonPath, "utf-8"));
35480
35975
  if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
@@ -35614,7 +36109,7 @@ __export(relaunch_exports, {
35614
36109
  resolveRelaunchEntry: () => resolveRelaunchEntry,
35615
36110
  updateRestartSummary: () => updateRestartSummary
35616
36111
  });
35617
- import { existsSync as existsSync32 } from "fs";
36112
+ import { existsSync as existsSync34 } from "fs";
35618
36113
  import { join as join35 } from "path";
35619
36114
  import { fileURLToPath as fileURLToPath2 } from "url";
35620
36115
  import { spawnSync } from "child_process";
@@ -35640,7 +36135,7 @@ function npmGlobalEntry() {
35640
36135
  const listed = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
35641
36136
  if (listed.status !== 0) return null;
35642
36137
  const entry = join35(listed.stdout.trim(), NPM_PACKAGE, "dist/index.js");
35643
- return existsSync32(entry) ? entry : null;
36138
+ return existsSync34(entry) ? entry : null;
35644
36139
  }
35645
36140
  function thisBundleEntry() {
35646
36141
  return fileURLToPath2(import.meta.url);
@@ -35650,10 +36145,10 @@ function resolveRelaunchEntry(toVersion) {
35650
36145
  (p) => Boolean(p)
35651
36146
  );
35652
36147
  for (const entry of candidates) {
35653
- if (!existsSync32(entry)) continue;
36148
+ if (!existsSync34(entry)) continue;
35654
36149
  if (readVersionNearEntry(entry) === toVersion) return entry;
35655
36150
  }
35656
- return candidates.find((p) => existsSync32(p)) ?? thisBundleEntry();
36151
+ return candidates.find((p) => existsSync34(p)) ?? thisBundleEntry();
35657
36152
  }
35658
36153
  function relaunchArgv(toVersion) {
35659
36154
  return [resolveRelaunchEntry(toVersion)];
@@ -36176,7 +36671,7 @@ __export(exports_exports, {
36176
36671
  handler: () => handler48
36177
36672
  });
36178
36673
  import chalk78 from "chalk";
36179
- import { existsSync as existsSync33 } from "fs";
36674
+ import { existsSync as existsSync35 } from "fs";
36180
36675
  import { join as join36 } from "path";
36181
36676
  function usage4() {
36182
36677
  console.log(chalk78.dim(" Usage:"));
@@ -36335,7 +36830,7 @@ function runMove(args, ctx) {
36335
36830
  }
36336
36831
  try {
36337
36832
  const destDir = resolveUserPath(dest);
36338
- if (!existsSync33(destDir)) {
36833
+ if (!existsSync35(destDir)) {
36339
36834
  }
36340
36835
  const event = moveExport(idOrName, destDir);
36341
36836
  console.log();
@@ -36707,10 +37202,9 @@ section: Settings
36707
37202
  handler: ../commands/onboard.ts
36708
37203
  ---
36709
37204
 
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.
37205
+ Start the progressive setup ladder. Each \`/onboard\` runs the next incomplete tier:
37206
+ profile (keyless) \u2192 domain research + API key \u2192 sample demo data \u2192 production CSV/folder path.
37207
+ Drag-drop a CSV or folder into the REPL anytime for real data.
36714
37208
  The profile is stored at \`~/.ntrp/profile.json\`. It flows into findings, NL answers, and demo data.`
36715
37209
  },
36716
37210
  {
@@ -37107,7 +37601,7 @@ hidden: true
37107
37601
  ---
37108
37602
 
37109
37603
  Mark every in-progress session as ended. Transcripts and dataset files stay.
37110
- Interactive ntrp only. Confirm with y/N, or pass \`--confirm\` in one-shot.`
37604
+ Interactive ntrp only. Confirm with \u23CE no (type yes to proceed), or pass \`--confirm\` in one-shot.`
37111
37605
  },
37112
37606
  {
37113
37607
  name: "deactivate-demo",
@@ -37370,7 +37864,7 @@ Remaining nuances merge into a custom_context paragraph that flows into all AI s
37370
37864
  });
37371
37865
 
37372
37866
  // src/ai/prompt-parts.ts
37373
- import { existsSync as existsSync34, readFileSync as readFileSync23 } from "fs";
37867
+ import { existsSync as existsSync36, readFileSync as readFileSync23 } from "fs";
37374
37868
  import { join as join37 } from "path";
37375
37869
  function buildCompanyProfileBlock() {
37376
37870
  const p = loadProfile();
@@ -37391,7 +37885,7 @@ function buildCompanyProfileBlock() {
37391
37885
  function loadAnalystFile() {
37392
37886
  const path = join37(ntrpHome(), ANALYST_FILE_NAME);
37393
37887
  try {
37394
- if (!existsSync34(path)) return null;
37888
+ if (!existsSync36(path)) return null;
37395
37889
  const raw = sanitizeExternalText(readFileSync23(path, "utf-8").trim());
37396
37890
  if (!raw) return null;
37397
37891
  if (raw.length <= ANALYST_FILE_MAX_CHARS) return raw;
@@ -37920,39 +38414,54 @@ var ingest_chat_exports = {};
37920
38414
  __export(ingest_chat_exports, {
37921
38415
  extractFilePath: () => extractFilePath,
37922
38416
  ingestFromChat: () => ingestFromChat,
38417
+ ingestPathFromChat: () => ingestPathFromChat,
37923
38418
  isDemoIntent: () => isDemoIntent,
38419
+ listCsvsInFolder: () => listCsvsInFolder,
37924
38420
  loadDemoFromChat: () => loadDemoFromChat,
37925
38421
  looksLikeFilePath: () => looksLikeFilePath
37926
38422
  });
37927
- import { existsSync as existsSync35 } from "fs";
37928
- import { basename as basename9, resolve as resolve9 } from "path";
38423
+ import { existsSync as existsSync37, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
38424
+ import { basename as basename10, join as join38, resolve as resolve9 } from "path";
37929
38425
  import { homedir as homedir8 } from "os";
37930
38426
  import chalk80 from "chalk";
37931
38427
  function extractFilePath(input) {
37932
- const trimmed = input.trim();
38428
+ const trimmed = input.trim().replace(/^["']|["']$/g, "");
38429
+ if (!trimmed) return null;
37933
38430
  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
38431
+ /^["'](.+)["']$/i,
38432
+ /^@(.+)$/i,
38433
+ /(?:here'?s|file|path|upload|folder|dir)[:\s]+["']?([^\s"']+)["']?/i,
38434
+ /^~\/\S+$/i,
38435
+ /^\.\.?\/\S+$/i,
38436
+ /^\/\S+$/i,
38437
+ /^[A-Za-z]:\\[^\s]+$/i,
38438
+ /^[^\s]+$/i
37942
38439
  ];
37943
38440
  for (const re of patterns) {
37944
38441
  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;
38442
+ const candidate = (m?.[1] ?? (re.test(trimmed) ? trimmed : null))?.replace(/^["']|["']$/g, "");
38443
+ if (!candidate) continue;
38444
+ if (!looksLikePathToken(candidate)) continue;
38445
+ const p = expandPath(candidate);
38446
+ if (existsSync37(p)) {
38447
+ try {
38448
+ const st = statSync5(p);
38449
+ if (st.isFile() || st.isDirectory()) return p;
38450
+ } catch {
38451
+ }
37952
38452
  }
37953
38453
  }
37954
38454
  return null;
37955
38455
  }
38456
+ function looksLikePathToken(token) {
38457
+ if (token.length < 2) return false;
38458
+ if (/^(yes|no|y|n|back|b|prev|cancel|skip|help|demo)$/i.test(token)) return false;
38459
+ if (token.includes("/") || token.includes("\\")) return true;
38460
+ if (token.startsWith("~")) return true;
38461
+ if (/^[A-Za-z]:/.test(token)) return true;
38462
+ if (/\.[A-Za-z0-9]{1,8}$/.test(token)) return true;
38463
+ return false;
38464
+ }
37956
38465
  function expandPath(p) {
37957
38466
  if (p.startsWith("~/")) return resolve9(homedir8(), p.slice(2));
37958
38467
  return resolve9(p);
@@ -37960,12 +38469,79 @@ function expandPath(p) {
37960
38469
  function looksLikeFilePath(input) {
37961
38470
  return extractFilePath(input) !== null;
37962
38471
  }
38472
+ function listCsvsInFolder(dir) {
38473
+ try {
38474
+ if (!statSync5(dir).isDirectory()) return [];
38475
+ return readdirSync6(dir).filter((name) => name.toLowerCase().endsWith(".csv")).map((name) => join38(dir, name)).sort();
38476
+ } catch {
38477
+ return [];
38478
+ }
38479
+ }
38480
+ async function ingestPathFromChat(ctx, rawPath) {
38481
+ let st;
38482
+ try {
38483
+ st = statSync5(rawPath);
38484
+ } catch {
38485
+ console.log(" " + chalk80.red(`Path not found: ${rawPath}`));
38486
+ return false;
38487
+ }
38488
+ if (st.isDirectory()) {
38489
+ const csvs = listCsvsInFolder(rawPath);
38490
+ if (csvs.length === 0) {
38491
+ console.log();
38492
+ console.log(" " + paint("accent", "Folder noted") + chalk80.dim(` \u2014 ${rawPath}`));
38493
+ console.log(" " + chalk80.dim("No CSV files one level deep. Drop a .csv path, or put exports in that folder."));
38494
+ ctx.attachments = [
38495
+ ...ctx.attachments ?? [],
38496
+ { path: rawPath, ingested_at: (/* @__PURE__ */ new Date()).toISOString() }
38497
+ ];
38498
+ saveSessionState(ctx);
38499
+ markProductionDataSeen();
38500
+ return false;
38501
+ }
38502
+ if (csvs.length === 1) {
38503
+ console.log(" " + chalk80.dim(`Found ${basename10(csvs[0])} in folder \u2014 ingesting.`));
38504
+ return ingestFromChat(ctx, csvs[0]);
38505
+ }
38506
+ if (!ctx.rl) {
38507
+ console.log(" " + chalk80.dim(`Found ${csvs.length} CSVs \u2014 re-run interactively to pick one.`));
38508
+ return false;
38509
+ }
38510
+ const prompts = createPromptSession(ctx.rl, ctx);
38511
+ try {
38512
+ const picked = await prompts.choose(
38513
+ "Which CSV in that folder?",
38514
+ csvs.map((p) => ({ value: p, label: basename10(p), description: p })),
38515
+ { default: csvs[0] }
38516
+ );
38517
+ return ingestFromChat(ctx, picked);
38518
+ } finally {
38519
+ prompts.close();
38520
+ }
38521
+ }
38522
+ if (st.isFile()) {
38523
+ if (!rawPath.toLowerCase().endsWith(".csv")) {
38524
+ console.log();
38525
+ console.log(" " + paint("accent", "Path noted") + chalk80.dim(` \u2014 ${rawPath}`));
38526
+ console.log(" " + chalk80.dim("NTRP ingests CSV exports today. Drop a .csv from that location when ready."));
38527
+ ctx.attachments = [
38528
+ ...ctx.attachments ?? [],
38529
+ { path: rawPath, ingested_at: (/* @__PURE__ */ new Date()).toISOString() }
38530
+ ];
38531
+ saveSessionState(ctx);
38532
+ markProductionDataSeen();
38533
+ return false;
38534
+ }
38535
+ return ingestFromChat(ctx, rawPath);
38536
+ }
38537
+ return false;
38538
+ }
37963
38539
  async function ingestFromChat(ctx, filePath) {
37964
38540
  if (!ctx.rl) {
37965
38541
  console.log(" " + chalk80.red("Ingest confirm requires interactive mode."));
37966
38542
  return false;
37967
38543
  }
37968
- const name = basename9(filePath);
38544
+ const name = basename10(filePath);
37969
38545
  const prompts = createPromptSession(ctx.rl, ctx);
37970
38546
  try {
37971
38547
  const ok = await prompts.confirm(`Ingest ${name} as CRM export?`, true);
@@ -38024,6 +38600,7 @@ async function ingestFromChat(ctx, filePath) {
38024
38600
  };
38025
38601
  invalidateGapAudit(ctx);
38026
38602
  saveSessionState(ctx);
38603
+ markProductionDataSeen();
38027
38604
  console.log();
38028
38605
  console.log(" " + paint("accent", "\u2713 Data loaded") + chalk80.dim(` \u2014 ${name}`));
38029
38606
  recordMessage(ctx, "user", `[ingested ${name}]`);
@@ -38099,6 +38676,7 @@ async function loadDemoFromChat(ctx, scenario, opts = {}) {
38099
38676
  counts,
38100
38677
  ingested_at: (/* @__PURE__ */ new Date()).toISOString()
38101
38678
  };
38679
+ markDemoDataSeen();
38102
38680
  if (!ctx.scope) {
38103
38681
  const { proposeScopeFromIntent: proposeScopeFromIntent2 } = await Promise.resolve().then(() => (init_scope(), scope_exports));
38104
38682
  const proposal = proposeScopeFromIntent2("demo pipeline and metrics");
@@ -38135,6 +38713,7 @@ var init_ingest_chat = __esm({
38135
38713
  init_compute2();
38136
38714
  init_theme();
38137
38715
  init_demo();
38716
+ init_onboard_tiers();
38138
38717
  }
38139
38718
  });
38140
38719
 
@@ -38482,7 +39061,7 @@ async function runFirstRunFork(ctx, options = {}) {
38482
39061
  }
38483
39062
  function printFirstRunChip() {
38484
39063
  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.")
39064
+ " " + 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
39065
  );
38487
39066
  console.log();
38488
39067
  }
@@ -38564,6 +39143,8 @@ async function loadFirstRunDemo(ctx, scenario) {
38564
39143
  counts,
38565
39144
  ingested_at: (/* @__PURE__ */ new Date()).toISOString()
38566
39145
  };
39146
+ const { markDemoDataSeen: markDemoDataSeen2 } = await Promise.resolve().then(() => (init_onboard_tiers(), onboard_tiers_exports));
39147
+ markDemoDataSeen2();
38567
39148
  const { saveSessionState: saveSessionState2 } = await Promise.resolve().then(() => (init_context2(), context_exports));
38568
39149
  saveSessionState2(ctx);
38569
39150
  return true;
@@ -38894,9 +39475,37 @@ async function conversationRouter(input, ctx) {
38894
39475
  const line = input.trim();
38895
39476
  if (!line) return { handled: true };
38896
39477
  if (FRESH_START_RE.test(line)) {
39478
+ if (ctx.rl) {
39479
+ const { createPromptSession: createPromptSession2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
39480
+ const prompts = createPromptSession2(ctx.rl, ctx);
39481
+ let ok = false;
39482
+ try {
39483
+ ok = await prompts.confirm(
39484
+ "Start a fresh analysis? Prior sessions stay saved.",
39485
+ true
39486
+ );
39487
+ } finally {
39488
+ prompts.close();
39489
+ }
39490
+ if (!ok) {
39491
+ console.log();
39492
+ console.log(
39493
+ " " + chalk84.dim("Staying on this session. Type ") + chalk84.cyan("/home") + chalk84.dim(" for the dashboard.")
39494
+ );
39495
+ console.log();
39496
+ return { handled: true };
39497
+ }
39498
+ recordMessage(ctx, "user", line);
39499
+ const { handler: handler51 } = await Promise.resolve().then(() => (init_new(), new_exports));
39500
+ const summary = await handler51([], ctx);
39501
+ return {
39502
+ handled: true,
39503
+ summary: typeof summary === "string" ? summary : "New analysis"
39504
+ };
39505
+ }
38897
39506
  console.log();
38898
39507
  console.log(
38899
- " " + chalk84.dim("Start a fresh analysis? This keeps prior sessions \u2014 say ") + chalk84.cyan("yes") + chalk84.dim(" to confirm or ") + chalk84.cyan("/home") + chalk84.dim(" for the dashboard.")
39508
+ " " + chalk84.dim("Start a fresh analysis with ") + chalk84.cyan("/new") + chalk84.dim(" \u2014 or ") + chalk84.cyan("/home") + chalk84.dim(" for the dashboard.")
38900
39509
  );
38901
39510
  console.log();
38902
39511
  return { handled: true };
@@ -38927,7 +39536,8 @@ async function conversationRouter(input, ctx) {
38927
39536
  const phase = resolveConversationPhase(ctx);
38928
39537
  if (looksLikeFilePath(line)) {
38929
39538
  const path = extractFilePath(line);
38930
- await ingestFromChat(ctx, path);
39539
+ const { ingestPathFromChat: ingestPathFromChat2 } = await Promise.resolve().then(() => (init_ingest_chat(), ingest_chat_exports));
39540
+ await ingestPathFromChat2(ctx, path);
38931
39541
  return { handled: true, summary: "Data ingested" };
38932
39542
  }
38933
39543
  if (isDemoIntent(line)) {
@@ -39853,7 +40463,7 @@ __export(repl_exports, {
39853
40463
  import { createInterface as createInterface2 } from "readline/promises";
39854
40464
  import { clearLine as clearLine2, cursorTo as cursorTo2 } from "readline";
39855
40465
  import chalk88 from "chalk";
39856
- import { join as join38 } from "path";
40466
+ import { join as join39 } from "path";
39857
40467
  function buildPrompt(ctx) {
39858
40468
  return buildConversationPrompt(ctx);
39859
40469
  }
@@ -40187,7 +40797,14 @@ function printHelp() {
40187
40797
  console.log();
40188
40798
  console.log(" " + sectionHeading("Keys"));
40189
40799
  console.log(
40190
- " " + paint("accent", "\u23CE") + chalk88.dim(" Runs the armed action shown at the prompt (yes, use demo data, go ahead, /connect).")
40800
+ " " + paint("accent", "\u23CE") + chalk88.dim(
40801
+ " Accepts the default. At the main prompt it runs the armed action (yes, use demo data, go ahead, /connect)."
40802
+ )
40803
+ );
40804
+ console.log(
40805
+ " " + chalk88.dim(
40806
+ " On yes/no questions and menus it accepts yes (or the recommended option). Type something else to decline or pick another path."
40807
+ )
40191
40808
  );
40192
40809
  console.log(
40193
40810
  " " + paint("accent", "b") + chalk88.dim(" / ") + paint("accent", "back") + chalk88.dim(" Steps back one confirm gate, or leaves a modal. Does not unload data or undo compute.")
@@ -40232,7 +40849,7 @@ function printHelp() {
40232
40849
  ["/remember <fact>", "Store a fact, a decision, or a preference"],
40233
40850
  ["/recall [topic]", "Show what NTRP stores about your business"],
40234
40851
  ["/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)"]
40852
+ [`${join39(ntrpHome(), ANALYST_FILE_NAME)}`, "Standing operator instructions (tone, priorities, house rules)"]
40236
40853
  ];
40237
40854
  const teachMaxW = Math.max(...teach.map(([c]) => c.length)) + 2;
40238
40855
  for (const [cmd, desc] of teach) {