@sonnechasser/ntrp 1.4.8 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +867 -191
  2. package/dist/mcp/server.js +1145 -1008
  3. package/package.json +2 -1
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
  );
@@ -12330,7 +12335,7 @@ var init_guide_slides = __esm({
12330
12335
  tagline: "Start with the question you care about \u2014 we'll mirror it back before we compute.",
12331
12336
  visual: {
12332
12337
  kind: "funnel",
12333
- caption: "When a dim \u23CE hint shows, bare Enter submits it. No hint means Enter does nothing.",
12338
+ caption: "When a dim \u23CE hint shows, bare Enter submits it. Type b (or back) to step back one gate. No hint means Enter does nothing.",
12334
12339
  funnel: [
12335
12340
  { label: "Type a question", widthPct: 100 },
12336
12341
  { label: "\u23CE yes (scope)", widthPct: 78 },
@@ -12341,6 +12346,8 @@ var init_guide_slides = __esm({
12341
12346
  lines: [
12342
12347
  `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
12348
  "",
12349
+ "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.",
12350
+ "",
12344
12351
  "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.",
12345
12352
  "",
12346
12353
  "Vital signs and SaaS metrics compute without an AI key. Every score that has a dollar translation shows it \u2014 that's the point of the stethoscope."
@@ -12349,7 +12356,7 @@ var init_guide_slides = __esm({
12349
12356
  "Power-user slash commands still work. They stay hidden from /help: /new, /diagnose, /metrics, /ingest, /session.",
12350
12357
  '"use demo data" loads a fitted sample book. Company profile is optional. In scripts, /demo --no-profile.',
12351
12358
  'After compute, the prompt returns to \u203A. Brief is the default depth; type "go deep" for the long read.',
12352
- "/home shows phase status. /help lists conversation shortcuts \u2014 not every command."
12359
+ "/home shows phase status. /help lists conversation shortcuts and the Keys legend (\u23CE, b/back, cancel)."
12353
12360
  ]
12354
12361
  };
12355
12362
  ASK = {
@@ -12369,7 +12376,7 @@ var init_guide_slides = __esm({
12369
12376
  "",
12370
12377
  '"what is our ARR?" or "why is this red?" need a loaded dataset. Narrative findings and /ask need a stored key \u2014 /connect pastes any provider key and we detect it.',
12371
12378
  "",
12372
- "Replay this tour with /deepdive, jump to one slide with /deepdive freshness (or nrr, arr, \u2026), or /deepdive guide for this how-to section. When the prompt shows a dim \u23CE hint (yes, use demo data, go ahead, /connect), bare Enter submits it."
12379
+ "Replay this tour with /deepdive, jump to one slide with /deepdive freshness (or nrr, arr, \u2026), or /deepdive guide for this how-to section. When the prompt shows a dim \u23CE hint (yes, use demo data, go ahead, /connect), bare Enter submits it \u2014 and b / back steps back one confirm gate (or the previous tour slide while you're in /deepdive)."
12373
12380
  ],
12374
12381
  deepdive: [
12375
12382
  "/connect ollama for a keyless local model. /connect --base-url <url> --id <name> for any OpenAI-compatible endpoint.",
@@ -12597,7 +12604,9 @@ function paintIntro(opts) {
12597
12604
  extraLines: [
12598
12605
  ...TOUR_INTRO_LINES,
12599
12606
  "",
12600
- chalk10.dim("No AI key needed for this tour. Enter advances; /deepdive (or d) opens more on any slide.")
12607
+ chalk10.dim(
12608
+ "No AI key needed for this tour. Enter advances; b goes to the previous slide; /deepdive (or d) opens more on any slide. After the tour, b / back steps back one confirm gate in the REPL."
12609
+ )
12601
12610
  ],
12602
12611
  footer: `${progressDots(opts.index, opts.total)} \u23CE next \xB7 q skip`
12603
12612
  });
@@ -12796,7 +12805,7 @@ async function offerFirstRunTour(ctx) {
12796
12805
  {
12797
12806
  value: "tour",
12798
12807
  label: "Take the tour",
12799
- description: "\u23CE through slides \u2014 /deepdive on any slide for more"
12808
+ description: "\u23CE next \xB7 b back on slides \u2014 /deepdive on any slide for more"
12800
12809
  },
12801
12810
  {
12802
12811
  value: "skip",
@@ -12862,7 +12871,7 @@ var init_metric_tour = __esm({
12862
12871
  "",
12863
12872
  "We're a stethoscope, not a hospital: we listen hard, translate scores into dollars, and point at plays \u2014 you decide the surgery."
12864
12873
  ];
12865
- TOUR_OFFER_BLURB = "How we think about SaaS numbers and five vitals \u2014 then how to talk to NTRP and ship work to your AI.";
12874
+ TOUR_OFFER_BLURB = "How we think about SaaS numbers and five vitals \u2014 then how to talk to NTRP and ship work to your AI. On slides, b goes back; after the tour, the same word steps back at confirm gates.";
12866
12875
  }
12867
12876
  });
12868
12877
 
@@ -15593,7 +15602,8 @@ function buildConversationPrompt(ctx) {
15593
15602
  const enterHint2 = action ? chalk11.dim(` \xB7 \u23CE ${action.hint}`) : "";
15594
15603
  return accentPromptHead(phase, sessionName) + chalk11.dim(stack) + enterHint2 + " ";
15595
15604
  }
15596
- const enterHint = action ? chalk11.dim(`\u23CE ${action.hint} `) : "";
15605
+ const modalBack = phase === "scope" || phase === "awaiting_data" || phase === "strategize" ? " \xB7 b back" : "";
15606
+ const enterHint = action ? chalk11.dim(`\u23CE ${action.hint}${modalBack} `) : "";
15597
15607
  return accentPromptHead(phase, sessionName) + enterHint;
15598
15608
  }
15599
15609
  var PROMPT_LABELS;
@@ -21813,10 +21823,12 @@ var init_agent_context = __esm({
21813
21823
  var scope_exports = {};
21814
21824
  __export(scope_exports, {
21815
21825
  confirmScope: () => confirmScope,
21826
+ isBackInput: () => isBackInput,
21816
21827
  isScopeAdjustInput: () => isScopeAdjustInput,
21817
21828
  isScopeConfirmInput: () => isScopeConfirmInput,
21818
21829
  isSkipClarifier: () => isSkipClarifier,
21819
- proposeScopeFromIntent: () => proposeScopeFromIntent
21830
+ proposeScopeFromIntent: () => proposeScopeFromIntent,
21831
+ unconfirmScope: () => unconfirmScope
21820
21832
  });
21821
21833
  function proposeScopeFromIntent(input) {
21822
21834
  const text = input.trim();
@@ -21861,12 +21873,20 @@ function confirmScope(ctx) {
21861
21873
  if (ctx.scope.primary_lens === "revenue_metrics" || ctx.scope.primary_lens === "gtm_health") {
21862
21874
  }
21863
21875
  }
21876
+ function unconfirmScope(ctx) {
21877
+ if (!ctx.scope?.confirmed_at) return;
21878
+ const { confirmed_at: _dropped, ...rest } = ctx.scope;
21879
+ ctx.scope = rest;
21880
+ }
21864
21881
  function isScopeConfirmInput(input) {
21865
21882
  return /^(yes|y|yeah|yep|correct|looks good|that'?s right|confirm|ok|okay|go)$/i.test(input.trim());
21866
21883
  }
21867
21884
  function isScopeAdjustInput(input) {
21868
21885
  return /^(adjust|change|refine|edit|no|nope)$/i.test(input.trim());
21869
21886
  }
21887
+ function isBackInput(input) {
21888
+ return /^(b|back|prev)\s*[.!]?\s*$/i.test(input.trim());
21889
+ }
21870
21890
  function isSkipClarifier(input) {
21871
21891
  return /^skip$/i.test(input.trim());
21872
21892
  }
@@ -22807,9 +22827,9 @@ async function handleGetSessionBrief(input) {
22807
22827
  if (!target) {
22808
22828
  return { error: `No session matching "${raw}".` };
22809
22829
  }
22810
- const { existsSync: existsSync36, readFileSync: readFileSync24 } = await import("fs");
22830
+ const { existsSync: existsSync38, readFileSync: readFileSync24 } = await import("fs");
22811
22831
  const briefPath = contextDocPathForSession2(target.id);
22812
- if (!existsSync36(briefPath)) {
22832
+ if (!existsSync38(briefPath)) {
22813
22833
  return {
22814
22834
  session_id: target.id,
22815
22835
  error: "No context brief on disk for this session (created before brief storage existed).",
@@ -23300,7 +23320,7 @@ function buildSituationalAwarenessBlock(ctx, opts = {}) {
23300
23320
  `Phase: ${phase} (${formatPhaseLabel(phase)}) \u2014 ${PHASE_MEANING[phase]}`
23301
23321
  ];
23302
23322
  if (channel === "think") {
23303
- lines.push("Channel: think \u2014 socratic partner; type done/cancel returns to \u203A");
23323
+ lines.push("Channel: think \u2014 socratic partner; type done/cancel/b back returns to \u203A");
23304
23324
  lines.push("Response mode: deep (tools on)");
23305
23325
  } else if (channel === "strategist") {
23306
23326
  lines.push("Channel: strategist \u2014 measurable plan engine owns sequencing");
@@ -23344,11 +23364,13 @@ function buildSituationalAwarenessBlock(ctx, opts = {}) {
23344
23364
  } else if (phase === "awaiting_data") {
23345
23365
  lines.push("Already done: scope confirmed; analysis not ready");
23346
23366
  lines.push(
23347
- 'Available now: tell the user to paste a CSV path, type "use demo data", or \u23CE go ahead when can_compute \u2014 you cannot open files'
23367
+ 'Available now: tell the user to paste a CSV path, type "use demo data", \u23CE go ahead when can_compute, or b/back to rewind the focus card \u2014 you cannot open files'
23348
23368
  );
23349
23369
  } else if (phase === "scope") {
23350
23370
  lines.push("Already done: intent proposed; waiting on confirm");
23351
- lines.push("Available now: wait for CLI confirm (\u23CE yes) \u2014 do not invent a different question");
23371
+ lines.push(
23372
+ "Available now: wait for CLI confirm (\u23CE yes) or b/back/adjust \u2014 do not invent a different question"
23373
+ );
23352
23374
  } else if (phase === "orient") {
23353
23375
  lines.push("Already done: nothing locked yet");
23354
23376
  lines.push("Available now: help the user name a focus; CLI will propose scope from their words");
@@ -23421,13 +23443,13 @@ var init_situation = __esm({
23421
23443
  FUNNEL_SPINE = "Funnel: orient \u2192 scope \u2192 data \u2192 compute \u2192 explore | side doors: think, strategy, ship";
23422
23444
  PHASE_MEANING = {
23423
23445
  orient: "setup \u2014 open chat; user has not locked a scope yet",
23424
- scope: "scope confirm \u2014 CLI owns yes/adjust; do not re-propose unless asked",
23425
- awaiting_data: "data gate \u2014 load CSV/demo or compute; CLI owns the gate",
23446
+ scope: "scope confirm \u2014 CLI owns yes / b back / adjust; do not re-propose unless asked",
23447
+ awaiting_data: "data gate \u2014 load CSV/demo or compute; b/back rewinds to scope confirm (does not unload data); CLI owns the gate",
23426
23448
  compute: "compute in progress \u2014 wait",
23427
23449
  explore: "analysis complete \u2014 free-form Q&A on this session's artifact",
23428
- think: "socratic think channel \u2014 return to \u203A via done/cancel",
23429
- strategize: "strategist flow \u2014 objective confirm or plan drafting",
23430
- deliver: "ship/handoff wizard \u2014 CLI owns write confirms"
23450
+ think: "socratic think channel \u2014 return to \u203A via done/cancel/b back",
23451
+ strategize: "strategist flow \u2014 objective confirm (yes / b back / adjust) or plan drafting",
23452
+ deliver: "ship/handoff wizard \u2014 CLI owns write confirms; b/back or cancel leaves the draft"
23431
23453
  };
23432
23454
  }
23433
23455
  });
@@ -24260,20 +24282,128 @@ var init_diagnose = __esm({
24260
24282
  });
24261
24283
 
24262
24284
  // src/ai/profile-draft.ts
24263
- async function draftCompanyProfile(seed, _ctx) {
24285
+ async function draftCompanyProfile(seed, ctx, opts = {}) {
24264
24286
  if (!seed.company_url && !seed.company_name) {
24265
24287
  throw new Error("draftCompanyProfile requires a URL or a company name");
24266
24288
  }
24267
- 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) {
24268
24329
  const out = {};
24269
- if (seed.company_name?.trim()) out.company_name = seed.company_name.trim();
24270
- 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;
24271
24356
  return out;
24272
24357
  }
24358
+ var SYSTEM_PROMPT4, ALLOWED_MOTIONS, ALLOWED_CRMS, ALLOWED_ENGAGEMENT;
24273
24359
  var init_profile_draft = __esm({
24274
24360
  "src/ai/profile-draft.ts"() {
24275
24361
  "use strict";
24362
+ init_repl_api();
24363
+ init_complete();
24276
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"]);
24277
24407
  }
24278
24408
  });
24279
24409
 
@@ -24515,7 +24645,7 @@ OUTPUT \u2014 STRICT JSON only, no fences, no commentary:
24515
24645
  });
24516
24646
 
24517
24647
  // src/ai/profile-clarify.ts
24518
- function stripFences3(text) {
24648
+ function stripFences4(text) {
24519
24649
  const trimmed = text.trim();
24520
24650
  const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
24521
24651
  if (fenced) return fenced[1].trim();
@@ -24535,7 +24665,7 @@ function formatDraft(draft) {
24535
24665
  return parts.join("\n");
24536
24666
  }
24537
24667
  function parseClarifyingQuestions(text) {
24538
- const cleaned = stripFences3(text);
24668
+ const cleaned = stripFences4(text);
24539
24669
  let parsed;
24540
24670
  try {
24541
24671
  parsed = JSON.parse(cleaned);
@@ -24632,7 +24762,7 @@ ${draftBlock}${userBlock}${answersBlock}
24632
24762
 
24633
24763
  Emit the refined profile now as STRICT JSON.`;
24634
24764
  const { text } = await llmCompleteText("onboard", REFINE_SYSTEM_PROMPT, userMessage, 2048, ctx);
24635
- const cleaned = stripFences3(text);
24765
+ const cleaned = stripFences4(text);
24636
24766
  let parsed;
24637
24767
  try {
24638
24768
  parsed = JSON.parse(cleaned);
@@ -24654,7 +24784,7 @@ function validateRefined(raw) {
24654
24784
  const targetCustomer = str2("target_customer");
24655
24785
  if (targetCustomer) out.target_customer = targetCustomer;
24656
24786
  const motion = str2("sales_motion")?.toLowerCase();
24657
- if (motion && ALLOWED_MOTIONS.has(motion)) {
24787
+ if (motion && ALLOWED_MOTIONS2.has(motion)) {
24658
24788
  out.sales_motion = motion;
24659
24789
  }
24660
24790
  const dealSize = str2("average_deal_size");
@@ -24664,14 +24794,14 @@ function validateRefined(raw) {
24664
24794
  out.sales_cycle_days = Math.round(cycleDays);
24665
24795
  }
24666
24796
  const crm = str2("primary_crm")?.toLowerCase();
24667
- if (crm && ALLOWED_CRMS.has(crm)) out.primary_crm = crm;
24797
+ if (crm && ALLOWED_CRMS2.has(crm)) out.primary_crm = crm;
24668
24798
  const engagement = str2("engagement_tool")?.toLowerCase();
24669
- if (engagement && ALLOWED_ENGAGEMENT.has(engagement)) out.engagement_tool = engagement;
24799
+ if (engagement && ALLOWED_ENGAGEMENT2.has(engagement)) out.engagement_tool = engagement;
24670
24800
  const userScope = str2("user_scope");
24671
24801
  if (userScope) out.user_scope = userScope;
24672
24802
  return out;
24673
24803
  }
24674
- 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;
24675
24805
  var init_profile_clarify = __esm({
24676
24806
  "src/ai/profile-clarify.ts"() {
24677
24807
  "use strict";
@@ -24736,9 +24866,9 @@ OUTPUT FORMAT \u2014 respond with STRICT JSON only, no preamble, no markdown fen
24736
24866
  "engagement_tool": "outreach",
24737
24867
  "user_scope": "string"
24738
24868
  }`;
24739
- ALLOWED_MOTIONS = /* @__PURE__ */ new Set(["plg", "smb_velocity", "mid_market", "enterprise"]);
24740
- ALLOWED_CRMS = /* @__PURE__ */ new Set(["salesforce", "hubspot", "pipedrive", "other"]);
24741
- 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"]);
24742
24872
  }
24743
24873
  });
24744
24874
 
@@ -24997,6 +25127,155 @@ var init_profile2 = __esm({
24997
25127
  }
24998
25128
  });
24999
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
+
25000
25279
  // src/conversation/voice-setup.ts
25001
25280
  var voice_setup_exports = {};
25002
25281
  __export(voice_setup_exports, {
@@ -25356,97 +25635,314 @@ __export(onboard_exports, {
25356
25635
  profileExists: () => profileExists
25357
25636
  });
25358
25637
  import chalk25 from "chalk";
25638
+ import { existsSync as existsSync26 } from "fs";
25639
+ import { basename as basename7 } from "path";
25359
25640
  async function handler5(args, ctx) {
25360
25641
  const { flags } = parseArgs2(args, ["force", "skip-brand"]);
25361
25642
  const force = getBool(flags, "force");
25362
25643
  const skipBrand = getBool(flags, "skip-brand");
25644
+ const tierOverride = normalizeTierFlag(getString(flags, "tier"));
25363
25645
  const priorProfile = loadProfile();
25364
25646
  const configured = isProfileConfigured(priorProfile);
25365
25647
  let existing = configured ? priorProfile : null;
25366
25648
  if (!skipBrand) printCenteredLogo();
25367
25649
  const session = createPromptSession(ctx.rl, ctx);
25368
25650
  try {
25369
- if (configured && !force) {
25370
- console.log();
25371
- console.log(" " + bold(`Profile already exists for ${priorProfile.company_name}.`));
25372
- const overwrite = await session.confirm("Overwrite this profile?", false);
25373
- if (!overwrite) {
25374
- console.log(" " + chalk25.dim("Keeping existing profile."));
25375
- console.log(" " + chalk25.dim("To skip prompts, type ") + paint("accent", "/onboard --force"));
25376
- 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}`;
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();
25377
25673
  }
25674
+ } else if (configured && force && !tierOverride) {
25675
+ console.log();
25676
+ console.log(" " + bold(`Rebuilding profile for ${priorProfile.company_name}.`));
25378
25677
  existing = null;
25678
+ resetOnboardTierProgress();
25379
25679
  } else if (priorProfile && !configured) {
25380
25680
  console.log();
25381
- 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."));
25382
25682
  existing = null;
25683
+ resetOnboardTierProgress();
25383
25684
  }
25384
- printIntro();
25385
- await ensureLlmKeys(session);
25386
- const siteOrName = await session.askRequired(
25387
- "What is the company website? Or type the company name."
25388
- );
25389
- const isUrlish = looksLikeUrl(siteOrName);
25390
- const companyUrl = isUrlish ? normalizeUrl(siteOrName) : void 0;
25391
- const seedName = isUrlish ? void 0 : siteOrName.trim();
25392
- const seed = {
25393
- ...companyUrl ? { company_url: companyUrl } : {},
25394
- ...seedName ? { company_name: seedName } : {}
25395
- };
25396
- let draft = await draftCompanyProfile(seed);
25397
- draft = await fillMissingFields(session, draft, existing);
25398
- const canonicalName = draft.company_name ?? seedName ?? existing?.company_name ?? "";
25399
- const salesMotion = draft.sales_motion ?? existing?.sales_motion ?? "mid_market";
25400
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
25401
- let candidate = {
25402
- schema_version: 1,
25403
- company_name: canonicalName,
25404
- ...companyUrl ? { company_url: companyUrl } : existing?.company_url ? { company_url: existing.company_url } : {},
25405
- industry: draft.industry ?? existing?.industry ?? "",
25406
- product_description: draft.product_description ?? existing?.product_description ?? "",
25407
- target_customer: draft.target_customer ?? existing?.target_customer ?? "",
25408
- sales_motion: salesMotion,
25409
- ...draft.average_deal_size !== void 0 ? { average_deal_size: draft.average_deal_size } : {},
25410
- ...draft.sales_cycle_days !== void 0 ? { sales_cycle_days: draft.sales_cycle_days } : {},
25411
- ...draft.primary_crm !== void 0 ? { primary_crm: draft.primary_crm } : {},
25412
- ...draft.engagement_tool !== void 0 ? { engagement_tool: draft.engagement_tool } : {},
25413
- ...existing?.user_scope ? { user_scope: existing.user_scope } : {},
25414
- created_at: existing?.created_at ?? now2,
25415
- updated_at: now2
25416
- };
25417
- renderProfile(candidate);
25418
- candidate = await adaptiveRefine(session, candidate, ctx);
25419
- for (; ; ) {
25420
- renderProfile(candidate);
25421
- const action = (await session.ask("Is this profile correct? Type Y, edit, or redraft", { default: "Y" })).toLowerCase();
25422
- if (action === "y" || action === "yes" || action === "") break;
25423
- if (action === "redraft") {
25424
- const fresh = await fillMissingFields(session, { company_url: candidate.company_url }, null);
25425
- candidate = applyDraft(candidate, fresh);
25426
- continue;
25427
- }
25428
- if (action === "edit" || action === "e") {
25429
- candidate = await editLoop(session, candidate);
25430
- continue;
25431
- }
25432
- 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);
25433
25703
  }
25434
- saveProfile(candidate);
25435
- setConfigValue("sales-motion", candidate.sales_motion);
25436
- invalidateTaxonomy();
25437
- 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()) {
25438
25772
  console.log();
25439
25773
  console.log(" " + chalk25.green("\u2713") + " " + bold(`Profile saved for ${candidate.company_name}`));
25440
- console.log(" " + chalk25.dim(`~/.ntrp/profile.json`));
25774
+ console.log(" " + chalk25.dim("A key is already connected \u2014 running domain research now."));
25441
25775
  console.log();
25776
+ await runDomainResearchOnProfile(session, ctx, candidate);
25777
+ markOnboardTierComplete("domain");
25442
25778
  await offerInboxSkillSetup(session, { beat: "production" });
25443
- await offerFittedDemoAfterOnboard(session, ctx, candidate);
25444
- const { offerVoiceSetup: offerVoiceSetup2 } = await Promise.resolve().then(() => (init_voice_setup(), voice_setup_exports));
25445
- await offerVoiceSetup2(ctx, session);
25446
- return `Profile saved for ${candidate.company_name}`;
25447
- } finally {
25448
- 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}`;
25449
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;
25450
25946
  }
25451
25947
  async function fillMissingFields(session, draft, existing) {
25452
25948
  const out = { ...draft };
@@ -25668,16 +26164,10 @@ async function editField(session, p, field) {
25668
26164
  }
25669
26165
  }
25670
26166
  }
25671
- function printIntro() {
25672
- console.log(" " + bold("Let's set up your company profile."));
25673
- console.log(" " + chalk25.dim("NTRP will research your business so every answer,"));
25674
- console.log(" " + chalk25.dim("finding, and demo is tailored to your reality."));
25675
- console.log();
25676
- }
25677
26167
  function printKeylessNote() {
25678
- 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."));
25679
26169
  console.log(
25680
- " " + 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(".")
25681
26171
  );
25682
26172
  console.log();
25683
26173
  }
@@ -25687,9 +26177,9 @@ async function ensureLlmKeys(session) {
25687
26177
  const { providerLabel: providerLabel2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
25688
26178
  const { countAvailableEngines: countAvailableEngines2 } = await Promise.resolve().then(() => (init_session_state(), session_state_exports));
25689
26179
  console.log();
25690
- console.log(" " + bold("Connect a key") + chalk25.dim(" (optional. Press Enter to skip)"));
25691
- console.log(" " + chalk25.dim("Scores and metrics are local math. No AI is required."));
25692
- 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."));
25693
26183
  console.log(
25694
26184
  " " + chalk25.dim("Paste any provider API key. Anthropic, OpenAI, Groq, Gemini, Mistral, and others.")
25695
26185
  );
@@ -25781,6 +26271,8 @@ var init_onboard = __esm({
25781
26271
  init_profile_clarify();
25782
26272
  init_profile2();
25783
26273
  init_inbox_setup();
26274
+ init_onboard_tiers();
26275
+ init_path_safety();
25784
26276
  SALES_MOTION_CHOICES = [
25785
26277
  { value: "plg", label: "PLG \u2014 Product-Led Growth", description: PRESET_DESCRIPTIONS.plg },
25786
26278
  { value: "smb_velocity", label: "SMB Velocity \u2014 Fast cycles, high volume", description: PRESET_DESCRIPTIONS.smb_velocity },
@@ -25821,8 +26313,8 @@ __export(new_exports, {
25821
26313
  handler: () => handler6
25822
26314
  });
25823
26315
  import chalk26 from "chalk";
25824
- import { existsSync as existsSync25 } from "fs";
25825
- import { basename as basename7 } from "path";
26316
+ import { existsSync as existsSync27 } from "fs";
26317
+ import { basename as basename8 } from "path";
25826
26318
  async function handler6(args, ctx) {
25827
26319
  const { positional, flags } = parseArgs2(args, ["demo", "empty", "list-scenarios", "regen-taxonomy"]);
25828
26320
  let source = null;
@@ -25843,7 +26335,7 @@ async function handler6(args, ctx) {
25843
26335
  console.error(chalk26.red(" Usage: /new <file.csv> | --demo [--scenario <name>] | --empty [--lens health|metrics]"));
25844
26336
  return;
25845
26337
  }
25846
- if (source.kind === "file" && !existsSync25(source.path)) {
26338
+ if (source.kind === "file" && !existsSync27(source.path)) {
25847
26339
  console.error(chalk26.red(` File not found: ${source.path}`));
25848
26340
  return;
25849
26341
  }
@@ -25867,7 +26359,7 @@ async function handler6(args, ctx) {
25867
26359
  const sourceFlag = getString(flags, "source", "s");
25868
26360
  if (sourceFlag) passthrough.push("--source", sourceFlag);
25869
26361
  await ingest(passthrough, ctx);
25870
- datasetLabel2 = basename7(source.path);
26362
+ datasetLabel2 = basename8(source.path);
25871
26363
  datasetSource = source.path;
25872
26364
  } else if (source.kind === "demo") {
25873
26365
  const { handler: demo } = await Promise.resolve().then(() => (init_generate(), generate_exports));
@@ -26070,7 +26562,7 @@ __export(end_exports, {
26070
26562
  handler: () => handler7
26071
26563
  });
26072
26564
  import chalk27 from "chalk";
26073
- import { existsSync as existsSync26 } from "fs";
26565
+ import { existsSync as existsSync28 } from "fs";
26074
26566
  async function handler7(args, ctx) {
26075
26567
  if (args.length > 0) {
26076
26568
  console.error(chalk27.red(" Usage: /end"));
@@ -26107,10 +26599,10 @@ async function handler7(args, ctx) {
26107
26599
  if (summary) {
26108
26600
  console.log(" " + chalk27.dim(summary));
26109
26601
  }
26110
- if (existsSync26(transcriptPathForSession(endedId))) {
26602
+ if (existsSync28(transcriptPathForSession(endedId))) {
26111
26603
  console.log(" " + chalk27.dim("Transcript: ") + chalk27.dim(transcriptPathForSession(endedId)));
26112
26604
  }
26113
- if (existsSync26(contextDocPathForSession(endedId))) {
26605
+ if (existsSync28(contextDocPathForSession(endedId))) {
26114
26606
  console.log(" " + chalk27.dim("Context brief: ") + chalk27.dim(contextDocPathForSession(endedId)));
26115
26607
  }
26116
26608
  console.log();
@@ -26132,7 +26624,7 @@ __export(session_exports, {
26132
26624
  });
26133
26625
  import chalk28 from "chalk";
26134
26626
  import { join as join23 } from "path";
26135
- import { existsSync as existsSync27 } from "fs";
26627
+ import { existsSync as existsSync29 } from "fs";
26136
26628
  async function handler8(args, ctx) {
26137
26629
  const sub = args[0];
26138
26630
  if (!sub) return listSessionsView(ctx);
@@ -26278,7 +26770,7 @@ async function pickUp(idArg, ctx) {
26278
26770
  );
26279
26771
  }
26280
26772
  const contextPath = contextDocPathForSession(target.id);
26281
- if (existsSync27(contextPath)) {
26773
+ if (existsSync29(contextPath)) {
26282
26774
  console.log(" " + chalk28.dim("Context brief: ") + chalk28.dim(contextPath));
26283
26775
  }
26284
26776
  console.log();
@@ -27230,7 +27722,7 @@ var init_bundle = __esm({
27230
27722
 
27231
27723
  // src/repositories/markdown.ts
27232
27724
  import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync17 } from "fs";
27233
- 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";
27234
27726
  import { stringify as stringifyYaml2 } from "yaml";
27235
27727
  function renderMarkdownFiles(pkg) {
27236
27728
  const bundleJson = JSON.stringify(pkg, null, 2) + "\n";
@@ -27420,7 +27912,7 @@ function getRootPath(target) {
27420
27912
  return resolve8(target.directory ?? `ntrp-repository-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
27421
27913
  }
27422
27914
  function safeFilename(value) {
27423
- 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";
27424
27916
  }
27425
27917
  function escapeSummary(value) {
27426
27918
  return value.replace(/[<>]/g, "");
@@ -29957,7 +30449,7 @@ async function handleStrategizeFlow(input, ctx) {
29957
30449
  " " + chalk37.dim("That looks like a question. A strategy objective is waiting.")
29958
30450
  );
29959
30451
  console.log(
29960
- " " + chalk37.dim("Type ") + chalk37.cyan("yes") + chalk37.dim(" to make the plan. Type ") + chalk37.cyan("adjust") + chalk37.dim(" to change the objective. Type ") + chalk37.cyan("cancel") + chalk37.dim(" to answer questions first.")
30452
+ " " + 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.")
29961
30453
  );
29962
30454
  console.log();
29963
30455
  return "Awaiting confirm";
@@ -29970,7 +30462,7 @@ async function handleStrategizeFlow(input, ctx) {
29970
30462
  }
29971
30463
  console.log();
29972
30464
  console.log(
29973
- " " + chalk37.dim("Type ") + chalk37.cyan("yes") + chalk37.dim(" to make the plan. Type ") + chalk37.cyan("adjust") + chalk37.dim(" to change the objective. Type ") + chalk37.cyan("cancel") + chalk37.dim(" to stop.")
30465
+ " " + 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.")
29974
30466
  );
29975
30467
  console.log();
29976
30468
  return "Awaiting confirm";
@@ -30147,7 +30639,7 @@ function printObjectiveCard(ctx, objective, proposed) {
30147
30639
  );
30148
30640
  console.log();
30149
30641
  console.log(
30150
- " " + chalk37.dim("Confirm? ") + chalk37.cyan("\u23CE yes") + chalk37.dim(" \xB7 ") + chalk37.cyan("adjust") + chalk37.dim(" \xB7 ") + chalk37.cyan("cancel")
30642
+ " " + chalk37.dim("Confirm? ") + chalk37.cyan("\u23CE yes") + chalk37.dim(" \xB7 ") + chalk37.cyan("b back") + chalk37.dim(" \xB7 ") + chalk37.cyan("adjust") + chalk37.dim(" \xB7 ") + chalk37.cyan("cancel")
30151
30643
  );
30152
30644
  console.log();
30153
30645
  }
@@ -31950,7 +32442,7 @@ var init_checkout = __esm({
31950
32442
  });
31951
32443
 
31952
32444
  // src/services/setup.ts
31953
- 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";
31954
32446
  import { join as join29 } from "path";
31955
32447
  function setupCheck() {
31956
32448
  const home = ntrpHome();
@@ -32515,7 +33007,7 @@ function printScopeProposal(ctx) {
32515
33007
  }
32516
33008
  console.log();
32517
33009
  console.log(
32518
- " " + chalk52.dim("Confirm? ") + chalk52.cyan("\u23CE yes") + chalk52.dim(" \xB7 ") + chalk52.cyan("adjust")
33010
+ " " + chalk52.dim("Confirm? ") + chalk52.cyan("\u23CE yes") + chalk52.dim(" \xB7 ") + chalk52.cyan("b back") + chalk52.dim(" \xB7 ") + chalk52.cyan("adjust")
32519
33011
  );
32520
33012
  console.log();
32521
33013
  }
@@ -33386,7 +33878,7 @@ var init_metrics = __esm({
33386
33878
  });
33387
33879
 
33388
33880
  // src/ai/feedback-apply.ts
33389
- function stripFences4(text) {
33881
+ function stripFences5(text) {
33390
33882
  const trimmed = text.trim();
33391
33883
  const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
33392
33884
  if (fenced) return fenced[1].trim();
@@ -33416,8 +33908,8 @@ OPERATOR FEEDBACK:
33416
33908
  "${feedbackText}"
33417
33909
 
33418
33910
  Apply the feedback now as STRICT JSON.`;
33419
- const { text } = await llmCompleteText("feedback", SYSTEM_PROMPT4, userMessage, 1024, ctx);
33420
- const cleaned = stripFences4(text);
33911
+ const { text } = await llmCompleteText("feedback", SYSTEM_PROMPT5, userMessage, 1024, ctx);
33912
+ const cleaned = stripFences5(text);
33421
33913
  let parsed;
33422
33914
  try {
33423
33915
  parsed = JSON.parse(cleaned);
@@ -33444,7 +33936,7 @@ function validatePatch(raw) {
33444
33936
  const targetCustomer = str2("target_customer");
33445
33937
  if (targetCustomer) out.target_customer = targetCustomer;
33446
33938
  const motion = str2("sales_motion")?.toLowerCase();
33447
- if (motion && ALLOWED_MOTIONS2.has(motion)) {
33939
+ if (motion && ALLOWED_MOTIONS3.has(motion)) {
33448
33940
  out.sales_motion = motion;
33449
33941
  }
33450
33942
  const dealSize = str2("average_deal_size");
@@ -33454,25 +33946,25 @@ function validatePatch(raw) {
33454
33946
  out.sales_cycle_days = Math.round(cycleDays);
33455
33947
  }
33456
33948
  const crm = str2("primary_crm")?.toLowerCase();
33457
- if (crm && ALLOWED_CRMS2.has(crm)) out.primary_crm = crm;
33949
+ if (crm && ALLOWED_CRMS3.has(crm)) out.primary_crm = crm;
33458
33950
  const engagement = str2("engagement_tool")?.toLowerCase();
33459
- if (engagement && ALLOWED_ENGAGEMENT2.has(engagement)) out.engagement_tool = engagement;
33951
+ if (engagement && ALLOWED_ENGAGEMENT3.has(engagement)) out.engagement_tool = engagement;
33460
33952
  const userScope = str2("user_scope");
33461
33953
  if (userScope) out.user_scope = userScope;
33462
33954
  const customContext = str2("custom_context");
33463
33955
  if (customContext) out.custom_context = customContext;
33464
33956
  return out;
33465
33957
  }
33466
- var ALLOWED_MOTIONS2, ALLOWED_CRMS2, ALLOWED_ENGAGEMENT2, SYSTEM_PROMPT4;
33958
+ var ALLOWED_MOTIONS3, ALLOWED_CRMS3, ALLOWED_ENGAGEMENT3, SYSTEM_PROMPT5;
33467
33959
  var init_feedback_apply = __esm({
33468
33960
  "src/ai/feedback-apply.ts"() {
33469
33961
  "use strict";
33470
33962
  init_repl_api();
33471
33963
  init_complete();
33472
- ALLOWED_MOTIONS2 = /* @__PURE__ */ new Set(["plg", "smb_velocity", "mid_market", "enterprise"]);
33473
- ALLOWED_CRMS2 = /* @__PURE__ */ new Set(["salesforce", "hubspot", "pipedrive", "other"]);
33474
- ALLOWED_ENGAGEMENT2 = /* @__PURE__ */ new Set(["outreach", "salesloft", "apollo", "none"]);
33475
- 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.
33476
33968
 
33477
33969
  YOUR JOB:
33478
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).
@@ -33957,7 +34449,7 @@ __export(sessions_exports, {
33957
34449
  handler: () => handler35
33958
34450
  });
33959
34451
  import chalk62 from "chalk";
33960
- import { existsSync as existsSync29 } from "fs";
34452
+ import { existsSync as existsSync31 } from "fs";
33961
34453
  async function handler35(args, _ctx) {
33962
34454
  const sub = args[0] ?? "list";
33963
34455
  if (sub === "list" || !args[0]) {
@@ -34064,12 +34556,12 @@ function showSession(idArg) {
34064
34556
  console.log();
34065
34557
  const transcriptPath = transcriptPathForSession(session.id);
34066
34558
  const contextPath = contextDocPathForSession(session.id);
34067
- if (existsSync29(transcriptPath) || existsSync29(contextPath)) {
34559
+ if (existsSync31(transcriptPath) || existsSync31(contextPath)) {
34068
34560
  console.log(" " + chalk62.dim("\u2500".repeat(40)));
34069
- if (existsSync29(contextPath)) {
34561
+ if (existsSync31(contextPath)) {
34070
34562
  console.log(" " + chalk62.dim("Context brief: ") + chalk62.dim(contextPath));
34071
34563
  }
34072
- if (existsSync29(transcriptPath)) {
34564
+ if (existsSync31(transcriptPath)) {
34073
34565
  console.log(" " + chalk62.dim("Full transcript: ") + chalk62.dim(transcriptPath));
34074
34566
  }
34075
34567
  console.log();
@@ -34338,6 +34830,7 @@ var init_privacy_notice = __esm({
34338
34830
  "Direct identifiers (names, emails, domains, deal names) are replaced with local tokens before any LLM HTTP call.",
34339
34831
  "The mapping stays in ~/.ntrp/privacy/ on this machine. The CLI shows real names; the provider never does.",
34340
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.",
34341
34834
  "Computed scores and dollar aggregates still go to the provider you connected.",
34342
34835
  "If you turn on web retrieval, named-account queries are refused. Generic GTM terms may go to Tavily or Brave.",
34343
34836
  "A custom --base-url receives the same tokenized payload.",
@@ -35405,20 +35898,20 @@ var init_model = __esm({
35405
35898
  });
35406
35899
 
35407
35900
  // src/config/update-check.ts
35408
- 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";
35409
35902
  import { join as join33 } from "path";
35410
35903
  function cachePath2() {
35411
35904
  return join33(ntrpHome(), "update-check.json");
35412
35905
  }
35413
35906
  function ensureDir7() {
35414
35907
  const dir = ntrpHome();
35415
- if (!existsSync30(dir)) {
35908
+ if (!existsSync32(dir)) {
35416
35909
  mkdirSync18(dir, { recursive: true });
35417
35910
  }
35418
35911
  }
35419
35912
  function loadUpdateCheckCache() {
35420
35913
  const path = cachePath2();
35421
- if (!existsSync30(path)) return null;
35914
+ if (!existsSync32(path)) return null;
35422
35915
  try {
35423
35916
  const parsed = JSON.parse(readFileSync21(path, "utf-8"));
35424
35917
  if (!parsed || typeof parsed !== "object" || typeof parsed.lastCheck !== "number" || typeof parsed.latestVersion !== "string") {
@@ -35439,7 +35932,7 @@ function isCacheFresh(cache2, ttlMs = CACHE_TTL_MS2) {
35439
35932
  }
35440
35933
  function invalidateUpdateCheckCache() {
35441
35934
  const path = cachePath2();
35442
- if (existsSync30(path)) {
35935
+ if (existsSync32(path)) {
35443
35936
  unlinkSync5(path);
35444
35937
  }
35445
35938
  }
@@ -35453,11 +35946,11 @@ var init_update_check = __esm({
35453
35946
  });
35454
35947
 
35455
35948
  // src/version.ts
35456
- import { existsSync as existsSync31, readFileSync as readFileSync22 } from "fs";
35949
+ import { existsSync as existsSync33, readFileSync as readFileSync22 } from "fs";
35457
35950
  import { dirname as dirname6, join as join34 } from "path";
35458
35951
  import { fileURLToPath } from "url";
35459
35952
  function readVersionFromPackageJson(packageJsonPath) {
35460
- if (!existsSync31(packageJsonPath)) return null;
35953
+ if (!existsSync33(packageJsonPath)) return null;
35461
35954
  try {
35462
35955
  const pkg = JSON.parse(readFileSync22(packageJsonPath, "utf-8"));
35463
35956
  if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
@@ -35597,7 +36090,7 @@ __export(relaunch_exports, {
35597
36090
  resolveRelaunchEntry: () => resolveRelaunchEntry,
35598
36091
  updateRestartSummary: () => updateRestartSummary
35599
36092
  });
35600
- import { existsSync as existsSync32 } from "fs";
36093
+ import { existsSync as existsSync34 } from "fs";
35601
36094
  import { join as join35 } from "path";
35602
36095
  import { fileURLToPath as fileURLToPath2 } from "url";
35603
36096
  import { spawnSync } from "child_process";
@@ -35623,7 +36116,7 @@ function npmGlobalEntry() {
35623
36116
  const listed = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
35624
36117
  if (listed.status !== 0) return null;
35625
36118
  const entry = join35(listed.stdout.trim(), NPM_PACKAGE, "dist/index.js");
35626
- return existsSync32(entry) ? entry : null;
36119
+ return existsSync34(entry) ? entry : null;
35627
36120
  }
35628
36121
  function thisBundleEntry() {
35629
36122
  return fileURLToPath2(import.meta.url);
@@ -35633,10 +36126,10 @@ function resolveRelaunchEntry(toVersion) {
35633
36126
  (p) => Boolean(p)
35634
36127
  );
35635
36128
  for (const entry of candidates) {
35636
- if (!existsSync32(entry)) continue;
36129
+ if (!existsSync34(entry)) continue;
35637
36130
  if (readVersionNearEntry(entry) === toVersion) return entry;
35638
36131
  }
35639
- return candidates.find((p) => existsSync32(p)) ?? thisBundleEntry();
36132
+ return candidates.find((p) => existsSync34(p)) ?? thisBundleEntry();
35640
36133
  }
35641
36134
  function relaunchArgv(toVersion) {
35642
36135
  return [resolveRelaunchEntry(toVersion)];
@@ -36159,7 +36652,7 @@ __export(exports_exports, {
36159
36652
  handler: () => handler48
36160
36653
  });
36161
36654
  import chalk78 from "chalk";
36162
- import { existsSync as existsSync33 } from "fs";
36655
+ import { existsSync as existsSync35 } from "fs";
36163
36656
  import { join as join36 } from "path";
36164
36657
  function usage4() {
36165
36658
  console.log(chalk78.dim(" Usage:"));
@@ -36318,7 +36811,7 @@ function runMove(args, ctx) {
36318
36811
  }
36319
36812
  try {
36320
36813
  const destDir = resolveUserPath(dest);
36321
- if (!existsSync33(destDir)) {
36814
+ if (!existsSync35(destDir)) {
36322
36815
  }
36323
36816
  const event = moveExport(idOrName, destDir);
36324
36817
  console.log();
@@ -36690,10 +37183,9 @@ section: Settings
36690
37183
  handler: ../commands/onboard.ts
36691
37184
  ---
36692
37185
 
36693
- Start the first-run wizard. It builds a company profile.
36694
- Connect one or two keys (Anthropic, OpenAI, or another provider).
36695
- Then answer a few seed questions. AI drafts industry, ICP, deal size, and stack guesses.
36696
- 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.
36697
37189
  The profile is stored at \`~/.ntrp/profile.json\`. It flows into findings, NL answers, and demo data.`
36698
37190
  },
36699
37191
  {
@@ -37032,6 +37524,7 @@ CLI slide deck: SaaS refresher, five vital signs (definition, formula, visual, d
37032
37524
  Bare \`/deepdive\` runs the full onboarding tour. Type \`/deepdive guide\` to jump to how to use NTRP (includes teaching the inbox once).
37033
37525
  Type \`/deepdive handoff\` for the ship-to-AI slide. Type \`/deepdive <metric>\` to jump to one metric card.
37034
37526
  Type \`/deepdive list\` to print the catalog. Works without an AI key. Re-run anytime from the homescreen.
37527
+ During the tour: Enter advances, \`b\` / \`back\` goes to the previous slide, \`q\` quits. After the tour, the same \`b\` / \`back\` word steps back one confirm gate in the REPL (scope, data, strategy) \u2014 it does not unload data or undo compute.
37035
37528
  Live values overlay when an analysis exists.`
37036
37529
  },
37037
37530
  {
@@ -37046,7 +37539,7 @@ handler: ../commands/thinkwithme.ts
37046
37539
 
37047
37540
  Open a dedicated \`think \u203A\` channel to explore ideas, challenge assumptions, and pull evidence from your data.
37048
37541
  Bare \`/thinkwithme\` enters the channel. Type \`/thinkwithme <topic>\` to seed the first turn.
37049
- Requires an analysis and an AI key (\`/connect\`). Type \`done\` or \`cancel\` to return to \u203A.
37542
+ Requires an analysis and an AI key (\`/connect\`). Type \`done\`, \`cancel\`, or \`b\` / \`back\` to return to \u203A.
37050
37543
  When you are ready to commit to a plan, say so or the partner can hand off to \`/strategy\`.`
37051
37544
  },
37052
37545
  {
@@ -37352,7 +37845,7 @@ Remaining nuances merge into a custom_context paragraph that flows into all AI s
37352
37845
  });
37353
37846
 
37354
37847
  // src/ai/prompt-parts.ts
37355
- import { existsSync as existsSync34, readFileSync as readFileSync23 } from "fs";
37848
+ import { existsSync as existsSync36, readFileSync as readFileSync23 } from "fs";
37356
37849
  import { join as join37 } from "path";
37357
37850
  function buildCompanyProfileBlock() {
37358
37851
  const p = loadProfile();
@@ -37373,7 +37866,7 @@ function buildCompanyProfileBlock() {
37373
37866
  function loadAnalystFile() {
37374
37867
  const path = join37(ntrpHome(), ANALYST_FILE_NAME);
37375
37868
  try {
37376
- if (!existsSync34(path)) return null;
37869
+ if (!existsSync36(path)) return null;
37377
37870
  const raw = sanitizeExternalText(readFileSync23(path, "utf-8").trim());
37378
37871
  if (!raw) return null;
37379
37872
  if (raw.length <= ANALYST_FILE_MAX_CHARS) return raw;
@@ -37902,39 +38395,54 @@ var ingest_chat_exports = {};
37902
38395
  __export(ingest_chat_exports, {
37903
38396
  extractFilePath: () => extractFilePath,
37904
38397
  ingestFromChat: () => ingestFromChat,
38398
+ ingestPathFromChat: () => ingestPathFromChat,
37905
38399
  isDemoIntent: () => isDemoIntent,
38400
+ listCsvsInFolder: () => listCsvsInFolder,
37906
38401
  loadDemoFromChat: () => loadDemoFromChat,
37907
38402
  looksLikeFilePath: () => looksLikeFilePath
37908
38403
  });
37909
- import { existsSync as existsSync35 } from "fs";
37910
- 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";
37911
38406
  import { homedir as homedir8 } from "os";
37912
38407
  import chalk80 from "chalk";
37913
38408
  function extractFilePath(input) {
37914
- const trimmed = input.trim();
38409
+ const trimmed = input.trim().replace(/^["']|["']$/g, "");
38410
+ if (!trimmed) return null;
37915
38411
  const patterns = [
37916
- /^["'](.+\.csv)["']$/i,
37917
- /^@(.+\.csv)$/i,
37918
- /(?:here'?s|file|path|upload)[:\s]+["']?([^\s"']+\.csv)["']?/i,
37919
- /^~\/\S+\.csv$/i,
37920
- /^\.\.?\/\S+\.csv$/i,
37921
- /^\/\S+\.csv$/i,
37922
- /^[A-Za-z]:\\[^\s]+\.csv$/i,
37923
- /^[^\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
37924
38420
  ];
37925
38421
  for (const re of patterns) {
37926
38422
  const m = trimmed.match(re);
37927
- if (m?.[1]) {
37928
- const p = expandPath(m[1]);
37929
- if (existsSync35(p)) return p;
37930
- }
37931
- if (!m?.[1] && re.test(trimmed) && trimmed.toLowerCase().endsWith(".csv")) {
37932
- const p = expandPath(trimmed.replace(/^["']|["']$/g, ""));
37933
- 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
+ }
37934
38433
  }
37935
38434
  }
37936
38435
  return null;
37937
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
+ }
37938
38446
  function expandPath(p) {
37939
38447
  if (p.startsWith("~/")) return resolve9(homedir8(), p.slice(2));
37940
38448
  return resolve9(p);
@@ -37942,12 +38450,79 @@ function expandPath(p) {
37942
38450
  function looksLikeFilePath(input) {
37943
38451
  return extractFilePath(input) !== null;
37944
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
+ }
37945
38520
  async function ingestFromChat(ctx, filePath) {
37946
38521
  if (!ctx.rl) {
37947
38522
  console.log(" " + chalk80.red("Ingest confirm requires interactive mode."));
37948
38523
  return false;
37949
38524
  }
37950
- const name = basename9(filePath);
38525
+ const name = basename10(filePath);
37951
38526
  const prompts = createPromptSession(ctx.rl, ctx);
37952
38527
  try {
37953
38528
  const ok = await prompts.confirm(`Ingest ${name} as CRM export?`, true);
@@ -38006,6 +38581,7 @@ async function ingestFromChat(ctx, filePath) {
38006
38581
  };
38007
38582
  invalidateGapAudit(ctx);
38008
38583
  saveSessionState(ctx);
38584
+ markProductionDataSeen();
38009
38585
  console.log();
38010
38586
  console.log(" " + paint("accent", "\u2713 Data loaded") + chalk80.dim(` \u2014 ${name}`));
38011
38587
  recordMessage(ctx, "user", `[ingested ${name}]`);
@@ -38081,6 +38657,7 @@ async function loadDemoFromChat(ctx, scenario, opts = {}) {
38081
38657
  counts,
38082
38658
  ingested_at: (/* @__PURE__ */ new Date()).toISOString()
38083
38659
  };
38660
+ markDemoDataSeen();
38084
38661
  if (!ctx.scope) {
38085
38662
  const { proposeScopeFromIntent: proposeScopeFromIntent2 } = await Promise.resolve().then(() => (init_scope(), scope_exports));
38086
38663
  const proposal = proposeScopeFromIntent2("demo pipeline and metrics");
@@ -38117,6 +38694,7 @@ var init_ingest_chat = __esm({
38117
38694
  init_compute2();
38118
38695
  init_theme();
38119
38696
  init_demo();
38697
+ init_onboard_tiers();
38120
38698
  }
38121
38699
  });
38122
38700
 
@@ -38464,7 +39042,7 @@ async function runFirstRunFork(ctx, options = {}) {
38464
39042
  }
38465
39043
  function printFirstRunChip() {
38466
39044
  console.log(
38467
- " " + 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).")
38468
39046
  );
38469
39047
  console.log();
38470
39048
  }
@@ -38546,6 +39124,8 @@ async function loadFirstRunDemo(ctx, scenario) {
38546
39124
  counts,
38547
39125
  ingested_at: (/* @__PURE__ */ new Date()).toISOString()
38548
39126
  };
39127
+ const { markDemoDataSeen: markDemoDataSeen2 } = await Promise.resolve().then(() => (init_onboard_tiers(), onboard_tiers_exports));
39128
+ markDemoDataSeen2();
38549
39129
  const { saveSessionState: saveSessionState2 } = await Promise.resolve().then(() => (init_context2(), context_exports));
38550
39130
  saveSessionState2(ctx);
38551
39131
  return true;
@@ -38792,6 +39372,83 @@ function popModalState(ctx) {
38792
39372
  }
38793
39373
  return popped;
38794
39374
  }
39375
+ function handleBackNavigation(ctx, line) {
39376
+ if (!isBackInput(line)) return null;
39377
+ const phase = resolveConversationPhase(ctx);
39378
+ if (phase === "orient" || phase === "explore" || phase === "compute") {
39379
+ return null;
39380
+ }
39381
+ if (phase === "scope") {
39382
+ console.log();
39383
+ console.log(" " + chalk84.dim("What should we focus on instead?"));
39384
+ ctx.scope = void 0;
39385
+ clearPendingAsk(ctx);
39386
+ saveSessionState(ctx);
39387
+ recordMessage(ctx, "user", line);
39388
+ recordMessage(ctx, "agent", "Scope cleared (back)");
39389
+ console.log();
39390
+ return { handled: true, summary: "Scope cleared" };
39391
+ }
39392
+ if (phase === "awaiting_data") {
39393
+ if (!ctx.scope?.confirmed_at) return null;
39394
+ const hadData = sessionHasData(ctx);
39395
+ const hadAnalysis = ctx.stage === "analyzed" || isAnalysisReady(ctx);
39396
+ unconfirmScope(ctx);
39397
+ saveSessionState(ctx);
39398
+ recordMessage(ctx, "user", line);
39399
+ recordMessage(ctx, "agent", "Scope unconfirmed (back)");
39400
+ printScopeProposal(ctx);
39401
+ if (hadData || hadAnalysis) {
39402
+ console.log(
39403
+ " " + chalk84.dim(
39404
+ "Focus gate rewound \u2014 loaded data and any analysis stay. `b` does not unload demo data or undo compute."
39405
+ )
39406
+ );
39407
+ console.log();
39408
+ }
39409
+ return { handled: true, summary: "Scope unconfirmed" };
39410
+ }
39411
+ if (phase === "strategize") {
39412
+ const state2 = ctx.strategistState;
39413
+ if (state2?.step === "objective_confirm") {
39414
+ state2.step = "objective_input";
39415
+ saveSessionState(ctx);
39416
+ recordMessage(ctx, "user", line);
39417
+ recordMessage(ctx, "agent", "Strategy objective adjust (back)");
39418
+ console.log();
39419
+ console.log(" " + chalk84.dim("What is the objective? State a finish line."));
39420
+ console.log();
39421
+ return { handled: true, summary: "Awaiting objective" };
39422
+ }
39423
+ const popped = popModalState(ctx);
39424
+ if (popped.length === 0) return null;
39425
+ saveSessionState(ctx);
39426
+ recordMessage(ctx, "user", line);
39427
+ recordMessage(ctx, "agent", `Back \u2014 dropped ${popped.join(", ")}`);
39428
+ const backTo = formatPhaseLabel(resolveConversationPhase(ctx));
39429
+ console.log();
39430
+ console.log(
39431
+ " " + chalk84.dim(`Back \u2014 dropped ${popped.join(" and ")}. At `) + chalk84.cyan(backTo) + chalk84.dim(".")
39432
+ );
39433
+ console.log();
39434
+ return { handled: true, summary: "Back" };
39435
+ }
39436
+ if (phase === "think" || phase === "deliver") {
39437
+ const popped = popModalState(ctx);
39438
+ if (popped.length === 0) return null;
39439
+ saveSessionState(ctx);
39440
+ recordMessage(ctx, "user", line);
39441
+ recordMessage(ctx, "agent", `Back \u2014 dropped ${popped.join(", ")}`);
39442
+ const backTo = formatPhaseLabel(resolveConversationPhase(ctx));
39443
+ console.log();
39444
+ console.log(
39445
+ " " + chalk84.dim(`Back \u2014 dropped ${popped.join(" and ")}. At `) + chalk84.cyan(backTo) + chalk84.dim(".")
39446
+ );
39447
+ console.log();
39448
+ return { handled: true, summary: "Back" };
39449
+ }
39450
+ return null;
39451
+ }
38795
39452
  async function conversationRouter(input, ctx) {
38796
39453
  if (ctx.oneShot || (ctx.wizardDepth ?? 0) > 0) {
38797
39454
  return { handled: false };
@@ -38821,6 +39478,8 @@ async function conversationRouter(input, ctx) {
38821
39478
  return { handled: true, summary: "Cancelled" };
38822
39479
  }
38823
39480
  }
39481
+ const backResult = handleBackNavigation(ctx, line);
39482
+ if (backResult) return backResult;
38824
39483
  if (/^\S+$/.test(line) && !/^\d$/.test(line)) {
38825
39484
  const { resolveSessionByToken: resolveSessionByToken2 } = await Promise.resolve().then(() => (init_context2(), context_exports));
38826
39485
  if (resolveSessionByToken2(line, { printErrors: false }) !== void 0) {
@@ -38830,7 +39489,8 @@ async function conversationRouter(input, ctx) {
38830
39489
  const phase = resolveConversationPhase(ctx);
38831
39490
  if (looksLikeFilePath(line)) {
38832
39491
  const path = extractFilePath(line);
38833
- await ingestFromChat(ctx, path);
39492
+ const { ingestPathFromChat: ingestPathFromChat2 } = await Promise.resolve().then(() => (init_ingest_chat(), ingest_chat_exports));
39493
+ await ingestPathFromChat2(ctx, path);
38834
39494
  return { handled: true, summary: "Data ingested" };
38835
39495
  }
38836
39496
  if (isDemoIntent(line)) {
@@ -38925,6 +39585,8 @@ var init_router = __esm({
38925
39585
  init_think_flow();
38926
39586
  init_context2();
38927
39587
  init_demo();
39588
+ init_scope();
39589
+ init_pending_ask();
38928
39590
  FRESH_START_RE = /\b(start (over|fresh)|new analysis|start again|reset session)\b/i;
38929
39591
  CANCEL_ESCAPE_RE = /^(cancel|stop|abort|quit|never\s?mind|nevermind|forget it)\s*[.!]?\s*$/i;
38930
39592
  }
@@ -39148,17 +39810,17 @@ var init_loop_guard2 = __esm({
39148
39810
  PHASE_GUIDES = {
39149
39811
  scope: {
39150
39812
  mode: "scope confirmation",
39151
- accepts: "\u23CE or yes (run with this focus) \xB7 adjust (restate it) \xB7 a new focus statement",
39152
- leave: "cancel drops the proposal \xB7 /home shows the dashboard"
39813
+ accepts: "\u23CE or yes (run with this focus) \xB7 b / back (clear it) \xB7 adjust (restate it) \xB7 a new focus statement",
39814
+ leave: "b / back or cancel drops the proposal \xB7 /home shows the dashboard"
39153
39815
  },
39154
39816
  awaiting_data: {
39155
39817
  mode: "data loading",
39156
39818
  accepts: 'a CSV path \xB7 "use demo data" \xB7 "go ahead" (compute once data is loaded) \xB7 \u23CE runs the action shown at the prompt',
39157
- leave: "cancel drops the scope \xB7 /home shows the dashboard"
39819
+ leave: "b / back rewinds to the focus confirm card (data stays) \xB7 cancel \xB7 /home shows the dashboard"
39158
39820
  },
39159
39821
  strategize: {
39160
39822
  mode: "strategy objective confirm",
39161
- accepts: "\u23CE or yes (build the plan) \xB7 adjust (restate the objective) \xB7 cancel",
39823
+ accepts: "\u23CE or yes (build the plan) \xB7 b / back or adjust (restate the objective) \xB7 cancel",
39162
39824
  leave: "cancel drops the strategy session and returns to Q&A"
39163
39825
  }
39164
39826
  };
@@ -39754,7 +40416,7 @@ __export(repl_exports, {
39754
40416
  import { createInterface as createInterface2 } from "readline/promises";
39755
40417
  import { clearLine as clearLine2, cursorTo as cursorTo2 } from "readline";
39756
40418
  import chalk88 from "chalk";
39757
- import { join as join38 } from "path";
40419
+ import { join as join39 } from "path";
39758
40420
  function buildPrompt(ctx) {
39759
40421
  return buildConversationPrompt(ctx);
39760
40422
  }
@@ -40086,6 +40748,20 @@ function printHelp() {
40086
40748
  console.log(" " + chalk88.dim("Type ") + paint("accent", '"ship a board deck"') + chalk88.dim(" to write a handoff."));
40087
40749
  console.log(" " + chalk88.dim("The ") + paint("accent", "\u203A") + chalk88.dim(" prompt shows brief or deep after analysis. Brief is the default."));
40088
40750
  console.log();
40751
+ console.log(" " + sectionHeading("Keys"));
40752
+ console.log(
40753
+ " " + paint("accent", "\u23CE") + chalk88.dim(" Runs the armed action shown at the prompt (yes, use demo data, go ahead, /connect).")
40754
+ );
40755
+ console.log(
40756
+ " " + 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.")
40757
+ );
40758
+ console.log(
40759
+ " " + paint("accent", "cancel") + chalk88.dim(" Drops the current modal overlay (strategy, think, ship, unconfirmed scope).")
40760
+ );
40761
+ console.log(
40762
+ " " + chalk88.dim("On ") + paint("accent", "/deepdive") + chalk88.dim(" slides, ") + paint("accent", "b") + chalk88.dim(" moves to the previous slide.")
40763
+ );
40764
+ console.log();
40089
40765
  console.log(" " + sectionHeading("Shortcuts"));
40090
40766
  const shortcuts = [
40091
40767
  ["/home", "Show status"],
@@ -40119,7 +40795,7 @@ function printHelp() {
40119
40795
  ["/remember <fact>", "Store a fact, a decision, or a preference"],
40120
40796
  ["/recall [topic]", "Show what NTRP stores about your business"],
40121
40797
  ["/rate good|bad <note>", "Correct the last answer. A bad note becomes a calibration"],
40122
- [`${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)"]
40123
40799
  ];
40124
40800
  const teachMaxW = Math.max(...teach.map(([c]) => c.length)) + 2;
40125
40801
  for (const [cmd, desc] of teach) {