@sonnechasser/ntrp 1.3.8 → 1.3.9

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 +2009 -1826
  2. package/dist/mcp/server.js +37 -13
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9861,1157 +9861,1474 @@ var init_layout = __esm({
9861
9861
  }
9862
9862
  });
9863
9863
 
9864
- // src/ui/banner.ts
9864
+ // src/ui/slides.ts
9865
9865
  import chalk6 from "chalk";
9866
- function renderLogo() {
9867
- return LOGO_LINES.map((line, i) => chalk6.hex(GRADIENT[i % GRADIENT.length])(line));
9868
- }
9869
- function printCenteredLogo() {
9870
- const width = termWidth();
9871
- const logo = renderLogo();
9872
- const maxLogoW = Math.max(...logo.map((l) => visibleWidth(l)));
9873
- const offset = " ".repeat(Math.max(0, Math.floor((width - maxLogoW) / 2)));
9874
- console.log();
9875
- for (const line of logo) console.log(offset + line);
9876
- console.log();
9866
+ function liveFromVital(vs) {
9867
+ const dollarLine = vs.dollar_value != null && vs.dollar_value > 0 ? `$${vs.dollar_value >= 1e6 ? `${(vs.dollar_value / 1e6).toFixed(1)}M` : vs.dollar_value >= 1e3 ? `${(vs.dollar_value / 1e3).toFixed(0)}K` : vs.dollar_value.toFixed(0)} ${vs.dollar_label ?? ""}`.trim() : void 0;
9868
+ return {
9869
+ formatted: String(Math.round(vs.score)),
9870
+ status: vs.status,
9871
+ dollarLine
9872
+ };
9877
9873
  }
9878
- function printReplHeader(version) {
9879
- const width = termWidth();
9880
- const cardW = Math.min(width - 2, 120);
9881
- const innerW = cardW - 2;
9882
- const outerPad = " ".repeat(Math.max(0, Math.floor((width - cardW) / 2)));
9883
- const logo = renderLogo();
9884
- const maxLogoW = Math.max(...logo.map((l) => visibleWidth(l)));
9885
- const logoOffset = " ".repeat(Math.max(0, Math.floor((cardW - maxLogoW) / 2)));
9886
- console.log();
9887
- for (const line of logo) console.log(outerPad + logoOffset + line);
9888
- console.log();
9889
- const versionTag = ` v${version} `;
9890
- const gap = Math.max(0, innerW - versionTag.length);
9891
- const gapL = Math.floor(gap / 2);
9892
- console.log(
9893
- outerPad + paint("border", `\u256D${"\u2500".repeat(gapL)}`) + chalk6.dim(versionTag) + paint("border", `${"\u2500".repeat(gap - gapL)}\u256E`)
9894
- );
9895
- console.log();
9874
+ function liveFromMetric(m) {
9875
+ return {
9876
+ formatted: m.formatted,
9877
+ status: m.status === "neutral" ? "neutral" : m.status,
9878
+ benchmarkNote: m.benchmark_note
9879
+ };
9896
9880
  }
9897
- var LOGO_LINES, TAGLINE;
9898
- var init_banner = __esm({
9899
- "src/ui/banner.ts"() {
9900
- "use strict";
9901
- init_theme();
9902
- init_layout();
9903
- LOGO_LINES = [
9904
- " \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
9905
- " \u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
9906
- " \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
9907
- " \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2550\u255D ",
9908
- " \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 ",
9909
- " \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D "
9910
- ];
9911
- TAGLINE = "It listens to the pipeline, then names the dollars at risk.";
9881
+ function clearSlideScreen() {
9882
+ if (process.stdout.isTTY) {
9883
+ process.stdout.write("\x1B[2J\x1B[H");
9912
9884
  }
9913
- });
9914
-
9915
- // src/license/trial-policy.ts
9916
- function getCheckoutUrl() {
9917
- return process.env.NTRP_CHECKOUT_URL ?? process.env.NTRP_PURCHASE_URL ?? SIGNUP_CHECKOUT_URL;
9918
9885
  }
9919
- function getUpgradeUrl() {
9920
- return process.env.NTRP_UPGRADE_URL ?? PRO_UPGRADE_CHECKOUT_URL ?? getCheckoutUrl();
9886
+ function slideCardWidth() {
9887
+ return resolveCardWidth({ min: 64, max: 100, margin: 4 });
9921
9888
  }
9922
- function evaluateTrial(activatedAt, now2 = /* @__PURE__ */ new Date()) {
9923
- const daysSince = Math.floor((now2.getTime() - activatedAt.getTime()) / 864e5);
9924
- if (daysSince >= TRIAL_GRACE_END_DAYS) {
9925
- return {
9926
- phase: "expired",
9927
- daysSinceActivation: daysSince,
9928
- daysUntilLockout: 0,
9929
- trialDaysRemaining: 0,
9930
- shouldNudge: false
9931
- };
9932
- }
9933
- if (daysSince >= TRIAL_FULL_DAYS) {
9934
- return {
9935
- phase: "grace",
9936
- daysSinceActivation: daysSince,
9937
- daysUntilLockout: TRIAL_GRACE_END_DAYS - daysSince,
9938
- trialDaysRemaining: 0,
9939
- shouldNudge: true
9940
- };
9889
+ function tonePaint(tone = "accent") {
9890
+ switch (tone) {
9891
+ case "green":
9892
+ return chalk6.hex("#22c55e");
9893
+ case "yellow":
9894
+ return chalk6.hex("#eab308");
9895
+ case "red":
9896
+ return chalk6.hex("#ef4444");
9897
+ case "neutral":
9898
+ return chalk6.dim;
9899
+ default:
9900
+ return (t) => paint("accent", t);
9941
9901
  }
9942
- const trialDaysRemaining = TRIAL_FULL_DAYS - daysSince;
9943
- return {
9944
- phase: "active",
9945
- daysSinceActivation: daysSince,
9946
- daysUntilLockout: TRIAL_GRACE_END_DAYS - daysSince,
9947
- trialDaysRemaining,
9948
- shouldNudge: daysSince >= TRIAL_ACTIVE_NUDGE_FROM_DAY
9949
- };
9950
9902
  }
9951
- function formatTrialActiveMessage(daysSince) {
9952
- const daysLeft = TRIAL_FULL_DAYS - daysSince;
9953
- if (daysLeft <= 0) return "trial license";
9954
- const dayWord = daysLeft === 1 ? "day" : "days";
9955
- return `trial license (${daysLeft} ${dayWord} remaining)`;
9903
+ function renderBarRow(bar, barWidth, labelW) {
9904
+ const fill = Math.max(0, Math.min(barWidth, Math.round(bar.value / 100 * barWidth)));
9905
+ const body = "\u2588".repeat(fill) + "\u2591".repeat(barWidth - fill);
9906
+ const colored = tonePaint(bar.tone)(body);
9907
+ const label = padRight(truncateVisible(bar.label, labelW), labelW);
9908
+ const pct = String(Math.round(bar.value)).padStart(3);
9909
+ return `${label} ${colored} ${chalk6.dim(pct)}`;
9956
9910
  }
9957
- var TRIAL_FULL_DAYS, TRIAL_GRACE_END_DAYS, TRIAL_ACTIVE_NUDGE_FROM_DAY, SIGNUP_CHECKOUT_URL, PRO_UPGRADE_CHECKOUT_URL;
9958
- var init_trial_policy = __esm({
9959
- "src/license/trial-policy.ts"() {
9960
- "use strict";
9961
- TRIAL_FULL_DAYS = 11;
9962
- TRIAL_GRACE_END_DAYS = 30;
9963
- TRIAL_ACTIVE_NUDGE_FROM_DAY = 8;
9964
- SIGNUP_CHECKOUT_URL = "https://sonnechasser.lemonsqueezy.com/checkout/buy/d62d35a2-a369-4cf5-a88b-328223866b5f";
9965
- PRO_UPGRADE_CHECKOUT_URL = "https://sonnechasser.lemonsqueezy.com/checkout/buy/3bd1da42-936f-49a6-a6c3-d11eee213884";
9911
+ function renderBars(bars, inner) {
9912
+ const labelW = Math.min(18, Math.max(...bars.map((b) => visibleWidth(b.label)), 8));
9913
+ const barWidth = Math.max(8, Math.min(28, inner - labelW - 6));
9914
+ return bars.map((b) => renderBarRow(b, barWidth, labelW));
9915
+ }
9916
+ function renderFunnel(steps, inner) {
9917
+ const maxBar = Math.max(12, Math.min(40, inner - 22));
9918
+ const lines = [];
9919
+ for (const step of steps) {
9920
+ const w = Math.max(2, Math.round(step.widthPct / 100 * maxBar));
9921
+ const bar = paint("accent", "\u2588".repeat(w));
9922
+ const label = truncateVisible(step.label, Math.max(8, inner - maxBar - 8));
9923
+ lines.push(`${padRight(label, Math.min(18, inner - maxBar - 6))} ${bar} ${chalk6.dim(`${step.widthPct}%`)}`);
9966
9924
  }
9967
- });
9968
-
9969
- // src/license/upgrade-whimsy.ts
9970
- function daysLabel(d) {
9971
- return `${d} day${d === 1 ? "" : "s"}`;
9925
+ return lines;
9972
9926
  }
9973
- function pick(items) {
9974
- return items[Math.floor(Math.random() * items.length)] ?? items[0];
9927
+ function renderWaterfall(steps, inner) {
9928
+ const maxAbs = Math.max(...steps.map((s) => Math.abs(s.cumulative)), 1);
9929
+ const barW = Math.max(10, Math.min(28, inner - 28));
9930
+ const lines = [];
9931
+ for (const step of steps) {
9932
+ const fill = Math.max(1, Math.round(Math.abs(step.cumulative) / maxAbs * barW));
9933
+ const bar = step.delta >= 0 ? chalk6.hex("#22c55e")("\u2588".repeat(fill)) : chalk6.hex("#ef4444")("\u2588".repeat(fill));
9934
+ const deltaStr = step.delta > 0 ? `+${step.delta}` : step.delta < 0 ? `${step.delta}` : `${step.delta}`;
9935
+ const deltaPainted = step.delta > 0 ? chalk6.hex("#22c55e")(deltaStr.padStart(5)) : step.delta < 0 ? chalk6.hex("#ef4444")(deltaStr.padStart(5)) : chalk6.dim(deltaStr.padStart(5));
9936
+ const label = padRight(truncateVisible(step.label, 14), 14);
9937
+ lines.push(`${label} ${deltaPainted} ${bar} ${chalk6.dim(`\u2192 ${step.cumulative}`)}`);
9938
+ }
9939
+ return lines;
9975
9940
  }
9976
- function randomGraceNudge(daysLeft) {
9977
- return pick(GRACE_NUDGES)(daysLeft);
9941
+ function renderLayerStack(layers, inner) {
9942
+ const lines = [];
9943
+ for (let i = 0; i < layers.length; i++) {
9944
+ const layer = layers[i];
9945
+ const marker2 = layer.highlight ? paint("accent", "\u25C6") : chalk6.dim("\u25C7");
9946
+ const text = layer.highlight ? bold(layer.label) : chalk6.dim(layer.label);
9947
+ lines.push(`${marker2} ${truncateVisible(text, inner - 4)}`);
9948
+ if (i < layers.length - 1) {
9949
+ lines.push(chalk6.dim(" \u2502"));
9950
+ }
9951
+ }
9952
+ return lines;
9978
9953
  }
9979
- function randomActiveTrialNudge(daysLeft) {
9980
- return pick(ACTIVE_TRIAL_NUDGES)(daysLeft);
9954
+ function renderLevers(levers, inner) {
9955
+ const cell = Math.floor((inner - 9) / 2);
9956
+ const lines = [];
9957
+ lines.push(chalk6.dim("\u250C" + "\u2500".repeat(Math.max(8, cell)) + "\u2510 \u250C" + "\u2500".repeat(Math.max(8, cell)) + "\u2510"));
9958
+ for (let i = 0; i < levers.length; i += 2) {
9959
+ const a = padRight(truncateVisible(levers[i] ?? "", cell - 2), cell - 2);
9960
+ const b = padRight(truncateVisible(levers[i + 1] ?? "", cell - 2), cell - 2);
9961
+ lines.push(
9962
+ `${paint("accent", "\u2502")} ${a} ${paint("accent", "\u2502")} ${paint("accent", "\u2502")} ${b} ${paint("accent", "\u2502")}`
9963
+ );
9964
+ }
9965
+ lines.push(chalk6.dim("\u2514" + "\u2500".repeat(Math.max(8, cell)) + "\u2518 \u2514" + "\u2500".repeat(Math.max(8, cell)) + "\u2518"));
9966
+ return lines;
9981
9967
  }
9982
- function randomCutoffNudge() {
9983
- return pick(CUTOFF_NUDGES);
9968
+ function renderGauge(score, inner, status) {
9969
+ const st = status ?? (score >= 80 ? "green" : score >= 60 ? "yellow" : "red");
9970
+ const bar = scoreBar(score, st === "neutral" ? "yellow" : st, Math.min(28, inner - 12));
9971
+ return [`${statusDot(st)} ${bar} ${bold(String(Math.round(score)))}`];
9984
9972
  }
9985
- function randomBlockedNudge() {
9986
- return pick(BLOCKED_WHILE_CUTOFF);
9973
+ function renderSplit(bars, inner) {
9974
+ if (bars.length < 2) return renderBars(bars, inner);
9975
+ const total = bars.reduce((s, b) => s + b.value, 0) || 100;
9976
+ const width = Math.max(16, Math.min(40, inner - 4));
9977
+ let used = 0;
9978
+ const parts = [];
9979
+ for (let i = 0; i < bars.length; i++) {
9980
+ const b = bars[i];
9981
+ const w = i === bars.length - 1 ? width - used : Math.max(1, Math.round(b.value / total * width));
9982
+ used += w;
9983
+ parts.push(tonePaint(b.tone)("\u2588".repeat(w)));
9984
+ }
9985
+ const legend = bars.map((b) => `${tonePaint(b.tone)("\u25CF")} ${b.label} ${chalk6.dim(`${Math.round(b.value)}%`)}`).join(" ");
9986
+ return [parts.join(""), truncateVisible(legend, inner)];
9987
9987
  }
9988
- function randomUpgradeHeadline(daysLeft) {
9989
- return pick(UPGRADE_HEADLINES)(daysLeft);
9988
+ function renderVisual(visual, inner) {
9989
+ const lines = [];
9990
+ switch (visual.kind) {
9991
+ case "bars":
9992
+ if (visual.bars?.length) lines.push(...renderBars(visual.bars, inner));
9993
+ break;
9994
+ case "funnel":
9995
+ if (visual.funnel?.length) lines.push(...renderFunnel(visual.funnel, inner));
9996
+ break;
9997
+ case "waterfall":
9998
+ if (visual.waterfall?.length) lines.push(...renderWaterfall(visual.waterfall, inner));
9999
+ break;
10000
+ case "layer_stack":
10001
+ if (visual.layers?.length) lines.push(...renderLayerStack(visual.layers, inner));
10002
+ break;
10003
+ case "levers":
10004
+ if (visual.levers?.length) lines.push(...renderLevers(visual.levers, inner));
10005
+ break;
10006
+ case "gauge":
10007
+ lines.push(...renderGauge(visual.gauge ?? 50, inner));
10008
+ if (visual.bars?.length) lines.push(...renderBars(visual.bars, inner));
10009
+ break;
10010
+ case "split":
10011
+ if (visual.bars?.length) lines.push(...renderSplit(visual.bars, inner));
10012
+ break;
10013
+ case "none":
10014
+ default:
10015
+ break;
10016
+ }
10017
+ if (visual.caption) {
10018
+ lines.push(chalk6.dim(truncateVisible(visual.caption, inner)));
10019
+ }
10020
+ return lines;
9990
10021
  }
9991
- function randomUpgradeSubtitle(reason) {
9992
- return pick(UPGRADE_SUBTITLES)(reason);
10022
+ function kindBadge(explainer) {
10023
+ if (explainer.kind === "vital") return badge("VITAL", "accent");
10024
+ return badge("SAAS", "info");
9993
10025
  }
9994
- function randomProActivatedLine() {
9995
- return pick(PRO_ACTIVATED_LINES);
10026
+ function statusToTone(status) {
10027
+ if (status === "green") return "success";
10028
+ if (status === "yellow") return "warning";
10029
+ if (status === "red") return "error";
10030
+ return "muted";
9996
10031
  }
9997
- var GRACE_NUDGES, ACTIVE_TRIAL_NUDGES, CUTOFF_NUDGES, BLOCKED_WHILE_CUTOFF, UPGRADE_HEADLINES, UPGRADE_SUBTITLES, PRO_ACTIVATED_LINES;
9998
- var init_upgrade_whimsy = __esm({
9999
- "src/license/upgrade-whimsy.ts"() {
10000
- "use strict";
10001
- init_trial_policy();
10002
- GRACE_NUDGES = [
10003
- (d) => `Your trial has ${daysLabel(d)} left. Type /upgrade to continue.`,
10004
- (d) => `Grace period: ${daysLabel(d)} left. Type /upgrade to stay on Pro.`,
10005
- (d) => `The trial ended. You have ${daysLabel(d)} of extra time. Type /upgrade.`,
10006
- (d) => `Status: ${daysLabel(d)} of grace left. Type /upgrade to continue.`,
10007
- (d) => `You have ${daysLabel(d)} left in the grace period. Type /upgrade.`,
10008
- (d) => `NTRP will stop after ${daysLabel(d)}. Type /upgrade to continue.`,
10009
- (d) => `The ${TRIAL_FULL_DAYS}-day trial has ended. Extra time: ${daysLabel(d)}. Type /upgrade.`,
10010
- (d) => `Grace ends in ${daysLabel(d)}. Type /upgrade to continue.`,
10011
- (d) => `You are in extra time. ${daysLabel(d)} left. Type /upgrade.`,
10012
- (d) => `${daysLabel(d)} left before trial end. Type /upgrade.`,
10013
- (d) => `Access continues for ${daysLabel(d)}. Type /upgrade to stay.`,
10014
- (d) => `Trial grace: ${daysLabel(d)} left. Type /upgrade.`,
10015
- (d) => `Decide in ${daysLabel(d)}. Type /upgrade to continue.`,
10016
- (d) => `Last notice: ${daysLabel(d)} of grace left. Type /upgrade.`
10017
- ];
10018
- ACTIVE_TRIAL_NUDGES = [
10019
- (d) => `Your trial has ${daysLabel(d)} left. Type /upgrade to continue.`,
10020
- (d) => `Trial status: ${daysLabel(d)} left. Type /upgrade when you decide.`,
10021
- (d) => `${daysLabel(d)} left on the trial. Type /upgrade to keep access.`,
10022
- (d) => `The trial ends in ${daysLabel(d)}. Type /upgrade to continue.`,
10023
- (d) => `You have ${daysLabel(d)} of trial time left. Type /upgrade to continue.`
10024
- ];
10025
- CUTOFF_NUDGES = [
10026
- "The trial has ended. Type /upgrade to continue.",
10027
- "Access is paused. Type /upgrade to resume.",
10028
- "The trial is over. Type /upgrade to restore access.",
10029
- "Trial end. Type /upgrade to continue.",
10030
- "NTRP is paused. Type /upgrade first.",
10031
- "The free trial has ended. Type /upgrade.",
10032
- "Access is closed. Type /upgrade to open it."
10033
- ];
10034
- BLOCKED_WHILE_CUTOFF = [
10035
- "This command needs Pro. Type /upgrade first.",
10036
- "Access is paused. Type /upgrade. Then try again.",
10037
- "This action is not available after trial end. Type /upgrade.",
10038
- "Pro is required for this command. Type /upgrade."
10039
- ];
10040
- UPGRADE_HEADLINES = [
10041
- () => "Upgrade to Pro",
10042
- () => "Trial status",
10043
- (d) => d !== void 0 ? `${daysLabel(d)} left` : "Upgrade required",
10044
- () => "Continue with Pro",
10045
- () => "Pro upgrade",
10046
- () => "Stay on Pro"
10047
- ];
10048
- UPGRADE_SUBTITLES = [
10049
- (r) => r === "expired" ? "The trial has ended. Complete checkout. Then paste your key." : r === "grace" ? `You had ${TRIAL_FULL_DAYS} trial days plus extra time. Type /upgrade to continue.` : "Complete checkout. Then paste your key.",
10050
- (r) => r === "expired" ? "Your data is unchanged. You need a Pro key." : r === "grace" ? "The grace period is limited. Type /upgrade to stay." : "No call is required. Paste the key from your email.",
10051
- (r) => r === "expired" ? "Your work is still here. Paste a Pro key to continue." : r === "grace" ? "Stay if you want Pro. Paste a key after checkout." : "Checkout takes about one minute."
10052
- ];
10053
- PRO_ACTIVATED_LINES = [
10054
- "Pro is active. Continue your work.",
10055
- "License activated. Continue.",
10056
- "Pro is ready. Continue from the last step.",
10057
- "Activation complete. Continue.",
10058
- "License stored. Continue.",
10059
- "You are on Pro. Continue."
10032
+ function pushWrapped(out, text, inner, indent = "") {
10033
+ for (const w of wrapWords(text, inner - indent.length)) {
10034
+ out.push(indent + w);
10035
+ }
10036
+ }
10037
+ function buildSlideContent(explainer, opts = {}) {
10038
+ const width = slideCardWidth();
10039
+ const inner = width - 4;
10040
+ const lines = [];
10041
+ const title = opts.titleOverride ?? (explainer ? explainer.label : "Metrics");
10042
+ if (explainer) {
10043
+ const headerBits = [
10044
+ kindBadge(explainer),
10045
+ chalk6.dim(explainer.group)
10060
10046
  ];
10047
+ if (opts.index != null && opts.total != null) {
10048
+ headerBits.push(chalk6.dim(`slide ${opts.index}/${opts.total}`));
10049
+ }
10050
+ lines.push(headerBits.join(chalk6.dim(" \xB7 ")));
10051
+ lines.push(chalk6.dim(explainer.tagline));
10052
+ lines.push("");
10053
+ if (opts.live) {
10054
+ const live = opts.live;
10055
+ const tone = statusToTone(live.status);
10056
+ const liveLine = `${statusDot(live.status)} ${bold("Your reading:")} ${bold(live.formatted)} ` + badge(String(live.status), tone);
10057
+ lines.push(truncateVisible(liveLine, inner));
10058
+ if (live.dollarLine) {
10059
+ lines.push(chalk6.dim(` $ ${live.dollarLine}`));
10060
+ }
10061
+ if (live.benchmarkNote) {
10062
+ lines.push(chalk6.dim(` ${live.benchmarkNote}`));
10063
+ }
10064
+ lines.push("");
10065
+ } else {
10066
+ const hint = explainer.benchmarkHint?.(opts.motion);
10067
+ if (hint) {
10068
+ lines.push(chalk6.dim(`Benchmark \xB7 ${hint}`));
10069
+ lines.push("");
10070
+ }
10071
+ }
10072
+ const visual = opts.visualOverride ?? explainer.visual;
10073
+ const visLines = renderVisual(visual, inner);
10074
+ if (visLines.length) {
10075
+ lines.push(...visLines);
10076
+ lines.push("");
10077
+ }
10078
+ lines.push(sectionHeading("What it means"));
10079
+ pushWrapped(lines, explainer.meaning, inner, " ");
10080
+ lines.push("");
10081
+ if (!opts.skipFormula) {
10082
+ lines.push(sectionHeading("How it's calculated"));
10083
+ pushWrapped(lines, explainer.how_computed, inner, " ");
10084
+ for (const f of explainer.formula_lines) {
10085
+ lines.push(paint("accent", ` ${f}`));
10086
+ }
10087
+ lines.push("");
10088
+ }
10089
+ if (opts.deepdive) {
10090
+ lines.push(sectionHeading("Deep dive"));
10091
+ pushWrapped(lines, explainer.expert_read, inner, " ");
10092
+ lines.push("");
10093
+ for (const bullet of explainer.deepdive) {
10094
+ pushWrapped(lines, `\xB7 ${bullet}`, inner, " ");
10095
+ }
10096
+ if (explainer.play_id) {
10097
+ lines.push("");
10098
+ lines.push(
10099
+ chalk6.dim(" Play: ") + paint("accent", explainer.play_id)
10100
+ );
10101
+ }
10102
+ lines.push("");
10103
+ }
10104
+ } else if (opts.visualOverride) {
10105
+ const visLines = renderVisual(opts.visualOverride, inner);
10106
+ if (visLines.length) {
10107
+ lines.push(...visLines);
10108
+ lines.push("");
10109
+ }
10061
10110
  }
10062
- });
10063
-
10064
- // src/license/normalize.ts
10065
- function normalizeLicenseKeyInput(raw) {
10066
- let key = raw.trim();
10067
- key = key.replace(/^\[>\s*/, "");
10068
- key = key.replace(/^\[\s*▶\s*/, "");
10069
- key = key.replace(/^▶\s*/, "");
10070
- key = key.replace(/\s+/g, "");
10071
- if (NTRP_PREFIX.test(key)) {
10072
- return `NTRP-${key.replace(/^NTRP-/i, "").toLowerCase()}`;
10111
+ if (opts.extraLines?.length) {
10112
+ for (const line of opts.extraLines) {
10113
+ if (line === "") lines.push("");
10114
+ else pushWrapped(lines, line, inner);
10115
+ }
10073
10116
  }
10074
- if (UUID_KEY.test(key)) return key.toLowerCase();
10075
- return key;
10076
- }
10077
- function detectLicenseFormat(key) {
10078
- if (NTRP_PREFIX.test(key)) return "ntrp";
10079
- if (UUID_KEY.test(key)) return "lemonsqueezy";
10080
- return "unknown";
10117
+ return { title, lines, width, inner };
10081
10118
  }
10082
- var NTRP_PREFIX, UUID_KEY;
10083
- var init_normalize = __esm({
10084
- "src/license/normalize.ts"() {
10085
- "use strict";
10086
- NTRP_PREFIX = /^NTRP-/i;
10087
- UUID_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10119
+ function renderMetricSlide(explainer, opts = {}) {
10120
+ const { title, lines, width, inner } = buildSlideContent(explainer, opts);
10121
+ const border = (s) => paint("border", s);
10122
+ const termW = termWidth();
10123
+ const outerPad = " ".repeat(Math.max(0, Math.floor((termW - width) / 2)));
10124
+ const out = [];
10125
+ out.push("");
10126
+ out.push(`${outerPad}${border(`\u256D${"\u2500".repeat(width - 2)}\u256E`)}`);
10127
+ out.push(
10128
+ `${outerPad}${border("\u2502 ")}${padRight(sectionHeading(title), inner)}${border(" \u2502")}`
10129
+ );
10130
+ out.push(`${outerPad}${border(`\u251C${"\u2500".repeat(width - 2)}\u2524`)}`);
10131
+ for (const row of lines) {
10132
+ out.push(
10133
+ `${outerPad}${border("\u2502 ")}${padRight(truncateVisible(row, inner), inner)}${border(" \u2502")}`
10134
+ );
10088
10135
  }
10089
- });
10090
-
10091
- // src/license/lemonsqueezy.ts
10092
- import { hostname } from "os";
10093
- function invalid(message) {
10094
- return {
10095
- valid: false,
10096
- edition: "trial",
10097
- expiresAt: null,
10098
- message
10099
- };
10136
+ const footer = opts.footer ?? (opts.deepdive ? `\u23CE next \xB7 b back \xB7 q quit` : `\u23CE next \xB7 /deepdive more \xB7 b back \xB7 q quit`);
10137
+ out.push(`${outerPad}${border(`\u251C${"\u2500".repeat(width - 2)}\u2524`)}`);
10138
+ out.push(
10139
+ `${outerPad}${border("\u2502 ")}${padRight(chalk6.dim(truncateVisible(footer, inner)), inner)}${border(" \u2502")}`
10140
+ );
10141
+ out.push(`${outerPad}${border(`\u2570${"\u2500".repeat(width - 2)}\u256F`)}`);
10142
+ out.push("");
10143
+ if (opts.asLines) return out;
10144
+ for (const line of out) console.log(line);
10100
10145
  }
10101
- function editionFromMeta(meta) {
10102
- const label = `${meta?.variant_name ?? ""} ${meta?.product_name ?? ""}`.toLowerCase();
10103
- if (label.includes("trial")) return "trial";
10104
- if (label.includes("team")) return "team";
10105
- return "pro";
10146
+ function progressDots(index, total) {
10147
+ const parts = [];
10148
+ for (let i = 1; i <= total; i++) {
10149
+ parts.push(i === index ? paint("accent", "\u25CF") : chalk6.dim("\u25CB"));
10150
+ }
10151
+ return parts.join("");
10106
10152
  }
10107
- function expiresAtFromKey(licenseKey) {
10108
- if (!licenseKey?.expires_at) return null;
10109
- const parsed = new Date(licenseKey.expires_at);
10110
- return Number.isNaN(parsed.getTime()) ? null : parsed;
10153
+ function printExplainerCatalogLine(explainer) {
10154
+ const kind = explainer.kind === "vital" ? paint("accent", "vital") : chalk6.dim("saas ");
10155
+ console.log(
10156
+ ` ${kind} ${bold(explainer.id.padEnd(20))} ${chalk6.dim(explainer.label)} \u2014 ${chalk6.dim(explainer.tagline)}`
10157
+ );
10111
10158
  }
10112
- function statusMessage(edition, expiresAt, meta) {
10113
- const product = meta?.variant_name || meta?.product_name;
10114
- const base = product ? `${edition} license (${product})` : `${edition} license`;
10115
- return expiresAt ? `${base} (expires ${expiresAt.toISOString().slice(0, 10)})` : base;
10159
+ function printGuideCatalogLine(slide) {
10160
+ console.log(
10161
+ ` ${paint("accent", "how ")} ${bold(slide.id.padEnd(20))} ${chalk6.dim(slide.label)} \u2014 ${chalk6.dim(slide.tagline)}`
10162
+ );
10116
10163
  }
10117
- function mapLsFailure(error, licenseKey) {
10118
- const status = licenseKey?.status?.toLowerCase();
10119
- if (status === "expired") {
10120
- return invalid("Time's up \u2014 /upgrade and paste your key.");
10121
- }
10122
- if (status === "disabled") {
10123
- return invalid("License disabled. Contact support or purchase a new license.");
10124
- }
10125
- if (error?.toLowerCase().includes("activation limit")) {
10126
- return invalid(
10127
- "License activation limit reached. Deactivate an old machine in your Lemon Squeezy account, then try again."
10128
- );
10129
- }
10130
- return invalid(error?.trim() || "Could not activate license key");
10164
+ function printDeepdiveHint(metricId, label) {
10165
+ const name = label ?? metricId;
10166
+ console.log(
10167
+ " " + chalk6.dim("How this number works: ") + paint("accent", `/deepdive ${metricId}`) + chalk6.dim(` \u2014 ${name}`)
10168
+ );
10169
+ console.log();
10131
10170
  }
10132
- async function postLicense(path, fields) {
10133
- const body = new URLSearchParams(fields);
10134
- const res = await fetch(`${LICENSE_API}/${path}`, {
10135
- method: "POST",
10136
- headers: { Accept: "application/json" },
10137
- body,
10138
- signal: AbortSignal.timeout(15e3)
10139
- });
10140
- const data = await res.json();
10141
- if (!res.ok && !data.error) {
10142
- throw new Error(`License server error (${res.status})`);
10171
+ var init_slides = __esm({
10172
+ "src/ui/slides.ts"() {
10173
+ "use strict";
10174
+ init_theme();
10175
+ init_layout();
10143
10176
  }
10144
- return data;
10145
- }
10146
- function defaultInstanceName() {
10147
- const host = hostname().replace(/[^\w.-]/g, "-").slice(0, 48) || "machine";
10148
- const user = (process.env.USER || process.env.USERNAME || "user").replace(/[^\w.-]/g, "-").slice(0, 15);
10149
- return `ntrp-${host}-${user}`;
10177
+ });
10178
+
10179
+ // src/ui/banner.ts
10180
+ import chalk7 from "chalk";
10181
+ function renderLogo() {
10182
+ return LOGO_LINES.map((line, i) => chalk7.hex(GRADIENT[i % GRADIENT.length])(line));
10150
10183
  }
10151
- async function activateLemonSqueezyLicense(licenseKey, instanceName = defaultInstanceName()) {
10152
- const data = await postLicense("activate", {
10153
- license_key: licenseKey,
10154
- instance_name: instanceName
10155
- });
10156
- if (!data.activated) {
10157
- return mapLsFailure(data.error, data.license_key);
10158
- }
10159
- const edition = editionFromMeta(data.meta);
10160
- const expiresAt = expiresAtFromKey(data.license_key);
10161
- const instanceId = data.instance?.id;
10162
- if (!instanceId) {
10163
- return invalid("Activation succeeded but no instance id was returned. Try again.");
10164
- }
10165
- return {
10166
- valid: true,
10167
- edition,
10168
- expiresAt,
10169
- message: statusMessage(edition, expiresAt, data.meta),
10170
- instanceId
10171
- };
10184
+ function printCenteredLogo() {
10185
+ const width = termWidth();
10186
+ const logo = renderLogo();
10187
+ const maxLogoW = Math.max(...logo.map((l) => visibleWidth(l)));
10188
+ const offset = " ".repeat(Math.max(0, Math.floor((width - maxLogoW) / 2)));
10189
+ console.log();
10190
+ for (const line of logo) console.log(offset + line);
10191
+ console.log();
10172
10192
  }
10173
- async function validateLemonSqueezyLicense(licenseKey, instanceId) {
10174
- const data = await postLicense("validate", {
10175
- license_key: licenseKey,
10176
- instance_id: instanceId
10177
- });
10178
- if (!data.valid) {
10179
- return mapLsFailure(data.error, data.license_key);
10180
- }
10181
- const edition = editionFromMeta(data.meta);
10182
- const expiresAt = expiresAtFromKey(data.license_key);
10183
- return {
10184
- valid: true,
10185
- edition,
10186
- expiresAt,
10187
- message: statusMessage(edition, expiresAt, data.meta)
10188
- };
10193
+ function printReplHeader(version) {
10194
+ const width = termWidth();
10195
+ const cardW = Math.min(width - 2, 120);
10196
+ const innerW = cardW - 2;
10197
+ const outerPad = " ".repeat(Math.max(0, Math.floor((width - cardW) / 2)));
10198
+ const logo = renderLogo();
10199
+ const maxLogoW = Math.max(...logo.map((l) => visibleWidth(l)));
10200
+ const logoOffset = " ".repeat(Math.max(0, Math.floor((cardW - maxLogoW) / 2)));
10201
+ console.log();
10202
+ for (const line of logo) console.log(outerPad + logoOffset + line);
10203
+ console.log();
10204
+ const versionTag = ` v${version} `;
10205
+ const gap = Math.max(0, innerW - versionTag.length);
10206
+ const gapL = Math.floor(gap / 2);
10207
+ console.log(
10208
+ outerPad + paint("border", `\u256D${"\u2500".repeat(gapL)}`) + chalk7.dim(versionTag) + paint("border", `${"\u2500".repeat(gap - gapL)}\u256E`)
10209
+ );
10210
+ console.log();
10189
10211
  }
10190
- var LICENSE_API;
10191
- var init_lemonsqueezy = __esm({
10192
- "src/license/lemonsqueezy.ts"() {
10212
+ var LOGO_LINES, TAGLINE;
10213
+ var init_banner = __esm({
10214
+ "src/ui/banner.ts"() {
10193
10215
  "use strict";
10194
- LICENSE_API = "https://api.lemonsqueezy.com/v1/licenses";
10216
+ init_theme();
10217
+ init_layout();
10218
+ LOGO_LINES = [
10219
+ " \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
10220
+ " \u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
10221
+ " \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
10222
+ " \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2550\u255D ",
10223
+ " \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 ",
10224
+ " \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D "
10225
+ ];
10226
+ TAGLINE = "It listens to the pipeline, then names the dollars at risk.";
10195
10227
  }
10196
10228
  });
10197
10229
 
10198
- // src/license/verify.ts
10199
- import { createHmac as createHmac2 } from "crypto";
10200
- function signingSecret() {
10201
- const secret2 = process.env.NTRP_SIGNING_SECRET;
10202
- if (!secret2) return null;
10203
- return secret2;
10204
- }
10205
- function validateLicenseKey(key) {
10206
- const invalid2 = (msg) => ({
10207
- valid: false,
10208
- edition: "trial",
10209
- expiresAt: null,
10210
- message: msg
10211
- });
10212
- if (!key || !key.startsWith("NTRP-")) {
10213
- return invalid2("Invalid key format");
10214
- }
10215
- const secret2 = signingSecret();
10216
- if (!secret2) {
10217
- return invalid2("Invalid key format");
10218
- }
10219
- const parts = key.replace("NTRP-", "").split("-");
10220
- if (parts.length !== 3) {
10221
- return invalid2("Invalid key format");
10222
- }
10223
- const [payload, meta, signature] = parts;
10224
- const dataToSign = `${payload}-${meta}`;
10225
- const expectedSig = createHmac2("sha256", secret2).update(dataToSign).digest("hex").slice(0, 8);
10226
- if (signature !== expectedSig) {
10227
- return invalid2("Invalid license key");
10228
- }
10229
- const editionCode = meta.slice(0, 2);
10230
- const expiryHex = meta.slice(2);
10231
- const edition = editionCode === "01" ? "pro" : editionCode === "02" ? "team" : "trial";
10232
- const expiryTs = parseInt(expiryHex, 16);
10233
- const expiresAt = expiryTs > 0 ? new Date(expiryTs * 1e3) : null;
10234
- if (expiresAt && expiresAt < /* @__PURE__ */ new Date() && edition !== "trial") {
10235
- return invalid2(`License expired on ${expiresAt.toISOString().slice(0, 10)}`);
10236
- }
10237
- return {
10238
- valid: true,
10239
- edition,
10240
- expiresAt,
10241
- message: edition === "trial" ? "trial license" : `${edition} license${expiresAt ? ` (expires ${expiresAt.toISOString().slice(0, 10)})` : " (perpetual)"}`
10242
- };
10243
- }
10244
- function trialActivatedAt() {
10245
- const stored = getConfigValue("license-activated-at");
10246
- if (stored) {
10247
- const parsed = new Date(stored);
10248
- if (!Number.isNaN(parsed.getTime())) return parsed;
10249
- }
10250
- const now2 = /* @__PURE__ */ new Date();
10251
- setConfigValue("license-activated-at", now2.toISOString());
10252
- return now2;
10230
+ // src/license/trial-policy.ts
10231
+ function getCheckoutUrl() {
10232
+ return process.env.NTRP_CHECKOUT_URL ?? process.env.NTRP_PURCHASE_URL ?? SIGNUP_CHECKOUT_URL;
10253
10233
  }
10254
- function recordLicenseActivation(edition) {
10255
- if (edition === "trial") {
10256
- setConfigValue("license-activated-at", (/* @__PURE__ */ new Date()).toISOString());
10257
- } else {
10258
- deleteConfigValue("license-activated-at");
10259
- }
10234
+ function getUpgradeUrl() {
10235
+ return process.env.NTRP_UPGRADE_URL ?? PRO_UPGRADE_CHECKOUT_URL ?? getCheckoutUrl();
10260
10236
  }
10261
- function applyTrialPolicy(result) {
10262
- if (result.edition !== "trial") return result;
10263
- const trial = evaluateTrial(trialActivatedAt());
10264
- if (trial.phase === "expired") {
10237
+ function evaluateTrial(activatedAt, now2 = /* @__PURE__ */ new Date()) {
10238
+ const daysSince = Math.floor((now2.getTime() - activatedAt.getTime()) / 864e5);
10239
+ if (daysSince >= TRIAL_GRACE_END_DAYS) {
10265
10240
  return {
10266
- valid: false,
10267
- edition: "trial",
10268
- expiresAt: result.expiresAt,
10269
- message: randomCutoffNudge(),
10270
- trialPhase: "expired",
10271
- shouldNudgeUpgrade: false,
10241
+ phase: "expired",
10242
+ daysSinceActivation: daysSince,
10272
10243
  daysUntilLockout: 0,
10273
- trialDaysRemaining: 0
10244
+ trialDaysRemaining: 0,
10245
+ shouldNudge: false
10274
10246
  };
10275
10247
  }
10276
- const message = trial.phase === "grace" ? `trial license (grace: ${trial.daysUntilLockout} day${trial.daysUntilLockout === 1 ? "" : "s"} left)` : formatTrialActiveMessage(trial.daysSinceActivation);
10277
- return {
10278
- ...result,
10279
- message,
10280
- trialPhase: trial.phase,
10281
- shouldNudgeUpgrade: trial.shouldNudge,
10282
- daysUntilLockout: trial.daysUntilLockout,
10283
- trialDaysRemaining: trial.trialDaysRemaining
10284
- };
10285
- }
10286
- function storedLicenseProvider(key) {
10287
- const configured = getConfigValue("license-provider");
10288
- if (configured === "ntrp" || configured === "lemonsqueezy") return configured;
10289
- return detectLicenseFormat(key) === "lemonsqueezy" ? "lemonsqueezy" : "ntrp";
10290
- }
10291
- function checkLemonSqueezyLicense(key) {
10292
- const instanceId = getConfigValue("license-instance-id");
10293
- if (!instanceId) {
10248
+ if (daysSince >= TRIAL_FULL_DAYS) {
10294
10249
  return {
10295
- valid: false,
10296
- edition: "trial",
10297
- expiresAt: null,
10298
- message: "License not activated on this machine. Run: ntrp activate <key>"
10250
+ phase: "grace",
10251
+ daysSinceActivation: daysSince,
10252
+ daysUntilLockout: TRIAL_GRACE_END_DAYS - daysSince,
10253
+ trialDaysRemaining: 0,
10254
+ shouldNudge: true
10299
10255
  };
10300
10256
  }
10301
- const edition = getConfigValue("license-edition") ?? "pro";
10302
- const base = {
10303
- valid: true,
10304
- edition,
10305
- expiresAt: null,
10306
- message: `${edition} license`
10257
+ const trialDaysRemaining = TRIAL_FULL_DAYS - daysSince;
10258
+ return {
10259
+ phase: "active",
10260
+ daysSinceActivation: daysSince,
10261
+ daysUntilLockout: TRIAL_GRACE_END_DAYS - daysSince,
10262
+ trialDaysRemaining,
10263
+ shouldNudge: daysSince >= TRIAL_ACTIVE_NUDGE_FROM_DAY
10307
10264
  };
10308
- return applyTrialPolicy(base);
10309
10265
  }
10310
- async function activateLicenseKey(rawKey) {
10311
- const key = normalizeLicenseKeyInput(rawKey);
10312
- const format = detectLicenseFormat(key);
10313
- if (format === "unknown") {
10314
- return {
10315
- valid: false,
10316
- edition: "trial",
10317
- expiresAt: null,
10318
- message: "Invalid key format"
10319
- };
10320
- }
10321
- if (format === "ntrp") {
10322
- const result = validateLicenseKey(key);
10323
- if (!result.valid) return result;
10324
- setConfigValue("license-key", key);
10325
- setConfigValue("license-provider", "ntrp");
10326
- deleteConfigValue("license-instance-id");
10327
- deleteConfigValue("license-edition");
10328
- recordLicenseActivation(result.edition);
10329
- return checkLicense();
10330
- }
10331
- const activated = await activateLemonSqueezyLicense(key);
10332
- if (!activated.valid || !activated.instanceId) return activated;
10333
- setConfigValue("license-key", key);
10334
- setConfigValue("license-provider", "lemonsqueezy");
10335
- setConfigValue("license-instance-id", activated.instanceId);
10336
- setConfigValue("license-edition", activated.edition);
10337
- recordLicenseActivation(activated.edition);
10338
- return checkLicense();
10266
+ function formatTrialActiveMessage(daysSince) {
10267
+ const daysLeft = TRIAL_FULL_DAYS - daysSince;
10268
+ if (daysLeft <= 0) return "trial license";
10269
+ const dayWord = daysLeft === 1 ? "day" : "days";
10270
+ return `trial license (${daysLeft} ${dayWord} remaining)`;
10339
10271
  }
10340
- async function refreshLicenseOnline() {
10341
- const key = getConfigValue("license-key");
10342
- if (!key || storedLicenseProvider(key) !== "lemonsqueezy") {
10343
- return checkLicense();
10344
- }
10345
- const instanceId = getConfigValue("license-instance-id");
10346
- if (!instanceId) return checkLicense();
10347
- try {
10348
- const result = await validateLemonSqueezyLicense(key, instanceId);
10349
- if (!result.valid) return result;
10350
- setConfigValue("license-edition", result.edition);
10351
- return checkLicense();
10352
- } catch {
10353
- return checkLicense();
10272
+ var TRIAL_FULL_DAYS, TRIAL_GRACE_END_DAYS, TRIAL_ACTIVE_NUDGE_FROM_DAY, SIGNUP_CHECKOUT_URL, PRO_UPGRADE_CHECKOUT_URL;
10273
+ var init_trial_policy = __esm({
10274
+ "src/license/trial-policy.ts"() {
10275
+ "use strict";
10276
+ TRIAL_FULL_DAYS = 11;
10277
+ TRIAL_GRACE_END_DAYS = 30;
10278
+ TRIAL_ACTIVE_NUDGE_FROM_DAY = 8;
10279
+ SIGNUP_CHECKOUT_URL = "https://sonnechasser.lemonsqueezy.com/checkout/buy/d62d35a2-a369-4cf5-a88b-328223866b5f";
10280
+ PRO_UPGRADE_CHECKOUT_URL = "https://sonnechasser.lemonsqueezy.com/checkout/buy/3bd1da42-936f-49a6-a6c3-d11eee213884";
10354
10281
  }
10282
+ });
10283
+
10284
+ // src/license/upgrade-whimsy.ts
10285
+ function daysLabel(d) {
10286
+ return `${d} day${d === 1 ? "" : "s"}`;
10355
10287
  }
10356
- function checkLicense() {
10357
- const key = getConfigValue("license-key");
10358
- if (!key) {
10359
- return {
10360
- valid: false,
10361
- edition: "trial",
10362
- expiresAt: null,
10363
- message: "No license key found. Run: ntrp activate <key>"
10364
- };
10365
- }
10366
- if (storedLicenseProvider(key) === "lemonsqueezy") {
10367
- return checkLemonSqueezyLicense(key);
10368
- }
10369
- const result = validateLicenseKey(key);
10370
- if (!result.valid || result.edition !== "trial") {
10371
- return result;
10372
- }
10373
- return applyTrialPolicy(result);
10288
+ function pick(items) {
10289
+ return items[Math.floor(Math.random() * items.length)] ?? items[0];
10374
10290
  }
10375
- var init_verify = __esm({
10376
- "src/license/verify.ts"() {
10291
+ function randomGraceNudge(daysLeft) {
10292
+ return pick(GRACE_NUDGES)(daysLeft);
10293
+ }
10294
+ function randomActiveTrialNudge(daysLeft) {
10295
+ return pick(ACTIVE_TRIAL_NUDGES)(daysLeft);
10296
+ }
10297
+ function randomCutoffNudge() {
10298
+ return pick(CUTOFF_NUDGES);
10299
+ }
10300
+ function randomBlockedNudge() {
10301
+ return pick(BLOCKED_WHILE_CUTOFF);
10302
+ }
10303
+ function randomUpgradeHeadline(daysLeft) {
10304
+ return pick(UPGRADE_HEADLINES)(daysLeft);
10305
+ }
10306
+ function randomUpgradeSubtitle(reason) {
10307
+ return pick(UPGRADE_SUBTITLES)(reason);
10308
+ }
10309
+ function randomProActivatedLine() {
10310
+ return pick(PRO_ACTIVATED_LINES);
10311
+ }
10312
+ var GRACE_NUDGES, ACTIVE_TRIAL_NUDGES, CUTOFF_NUDGES, BLOCKED_WHILE_CUTOFF, UPGRADE_HEADLINES, UPGRADE_SUBTITLES, PRO_ACTIVATED_LINES;
10313
+ var init_upgrade_whimsy = __esm({
10314
+ "src/license/upgrade-whimsy.ts"() {
10377
10315
  "use strict";
10378
- init_store();
10379
10316
  init_trial_policy();
10380
- init_upgrade_whimsy();
10381
- init_normalize();
10382
- init_lemonsqueezy();
10317
+ GRACE_NUDGES = [
10318
+ (d) => `Your trial has ${daysLabel(d)} left. Type /upgrade to continue.`,
10319
+ (d) => `Grace period: ${daysLabel(d)} left. Type /upgrade to stay on Pro.`,
10320
+ (d) => `The trial ended. You have ${daysLabel(d)} of extra time. Type /upgrade.`,
10321
+ (d) => `Status: ${daysLabel(d)} of grace left. Type /upgrade to continue.`,
10322
+ (d) => `You have ${daysLabel(d)} left in the grace period. Type /upgrade.`,
10323
+ (d) => `NTRP will stop after ${daysLabel(d)}. Type /upgrade to continue.`,
10324
+ (d) => `The ${TRIAL_FULL_DAYS}-day trial has ended. Extra time: ${daysLabel(d)}. Type /upgrade.`,
10325
+ (d) => `Grace ends in ${daysLabel(d)}. Type /upgrade to continue.`,
10326
+ (d) => `You are in extra time. ${daysLabel(d)} left. Type /upgrade.`,
10327
+ (d) => `${daysLabel(d)} left before trial end. Type /upgrade.`,
10328
+ (d) => `Access continues for ${daysLabel(d)}. Type /upgrade to stay.`,
10329
+ (d) => `Trial grace: ${daysLabel(d)} left. Type /upgrade.`,
10330
+ (d) => `Decide in ${daysLabel(d)}. Type /upgrade to continue.`,
10331
+ (d) => `Last notice: ${daysLabel(d)} of grace left. Type /upgrade.`
10332
+ ];
10333
+ ACTIVE_TRIAL_NUDGES = [
10334
+ (d) => `Your trial has ${daysLabel(d)} left. Type /upgrade to continue.`,
10335
+ (d) => `Trial status: ${daysLabel(d)} left. Type /upgrade when you decide.`,
10336
+ (d) => `${daysLabel(d)} left on the trial. Type /upgrade to keep access.`,
10337
+ (d) => `The trial ends in ${daysLabel(d)}. Type /upgrade to continue.`,
10338
+ (d) => `You have ${daysLabel(d)} of trial time left. Type /upgrade to continue.`
10339
+ ];
10340
+ CUTOFF_NUDGES = [
10341
+ "The trial has ended. Type /upgrade to continue.",
10342
+ "Access is paused. Type /upgrade to resume.",
10343
+ "The trial is over. Type /upgrade to restore access.",
10344
+ "Trial end. Type /upgrade to continue.",
10345
+ "NTRP is paused. Type /upgrade first.",
10346
+ "The free trial has ended. Type /upgrade.",
10347
+ "Access is closed. Type /upgrade to open it."
10348
+ ];
10349
+ BLOCKED_WHILE_CUTOFF = [
10350
+ "This command needs Pro. Type /upgrade first.",
10351
+ "Access is paused. Type /upgrade. Then try again.",
10352
+ "This action is not available after trial end. Type /upgrade.",
10353
+ "Pro is required for this command. Type /upgrade."
10354
+ ];
10355
+ UPGRADE_HEADLINES = [
10356
+ () => "Upgrade to Pro",
10357
+ () => "Trial status",
10358
+ (d) => d !== void 0 ? `${daysLabel(d)} left` : "Upgrade required",
10359
+ () => "Continue with Pro",
10360
+ () => "Pro upgrade",
10361
+ () => "Stay on Pro"
10362
+ ];
10363
+ UPGRADE_SUBTITLES = [
10364
+ (r) => r === "expired" ? "The trial has ended. Complete checkout. Then paste your key." : r === "grace" ? `You had ${TRIAL_FULL_DAYS} trial days plus extra time. Type /upgrade to continue.` : "Complete checkout. Then paste your key.",
10365
+ (r) => r === "expired" ? "Your data is unchanged. You need a Pro key." : r === "grace" ? "The grace period is limited. Type /upgrade to stay." : "No call is required. Paste the key from your email.",
10366
+ (r) => r === "expired" ? "Your work is still here. Paste a Pro key to continue." : r === "grace" ? "Stay if you want Pro. Paste a key after checkout." : "Checkout takes about one minute."
10367
+ ];
10368
+ PRO_ACTIVATED_LINES = [
10369
+ "Pro is active. Continue your work.",
10370
+ "License activated. Continue.",
10371
+ "Pro is ready. Continue from the last step.",
10372
+ "Activation complete. Continue.",
10373
+ "License stored. Continue.",
10374
+ "You are on Pro. Continue."
10375
+ ];
10383
10376
  }
10384
10377
  });
10385
10378
 
10386
- // src/ui/open-browser.ts
10387
- import { spawn } from "child_process";
10388
- import { platform } from "os";
10389
- function openInBrowser(url) {
10390
- return new Promise((resolve10, reject) => {
10391
- let cmd;
10392
- let args;
10393
- switch (platform()) {
10394
- case "darwin":
10395
- cmd = "open";
10396
- args = [url];
10397
- break;
10398
- case "win32":
10399
- cmd = "cmd";
10400
- args = ["/c", "start", "", url];
10401
- break;
10402
- default:
10403
- cmd = "xdg-open";
10404
- args = [url];
10405
- break;
10406
- }
10407
- const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
10408
- child.on("error", reject);
10409
- child.unref();
10410
- resolve10();
10411
- });
10379
+ // src/license/normalize.ts
10380
+ function normalizeLicenseKeyInput(raw) {
10381
+ let key = raw.trim();
10382
+ key = key.replace(/^\[>\s*/, "");
10383
+ key = key.replace(/^\[\s*▶\s*/, "");
10384
+ key = key.replace(/^▶\s*/, "");
10385
+ key = key.replace(/\s+/g, "");
10386
+ if (NTRP_PREFIX.test(key)) {
10387
+ return `NTRP-${key.replace(/^NTRP-/i, "").toLowerCase()}`;
10388
+ }
10389
+ if (UUID_KEY.test(key)) return key.toLowerCase();
10390
+ return key;
10412
10391
  }
10413
- var init_open_browser = __esm({
10414
- "src/ui/open-browser.ts"() {
10392
+ function detectLicenseFormat(key) {
10393
+ if (NTRP_PREFIX.test(key)) return "ntrp";
10394
+ if (UUID_KEY.test(key)) return "lemonsqueezy";
10395
+ return "unknown";
10396
+ }
10397
+ var NTRP_PREFIX, UUID_KEY;
10398
+ var init_normalize = __esm({
10399
+ "src/license/normalize.ts"() {
10415
10400
  "use strict";
10401
+ NTRP_PREFIX = /^NTRP-/i;
10402
+ UUID_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10416
10403
  }
10417
10404
  });
10418
10405
 
10419
- // src/license/upgrade.ts
10420
- var upgrade_exports = {};
10421
- __export(upgrade_exports, {
10422
- getCheckoutUrl: () => getCheckoutUrl,
10423
- getUpgradeUrl: () => getUpgradeUrl,
10424
- hasStoredLicenseKey: () => hasStoredLicenseKey,
10425
- isTrialCutoff: () => isTrialCutoff,
10426
- isTrialGrace: () => isTrialGrace,
10427
- openCheckoutInBrowser: () => openCheckoutInBrowser,
10428
- printActiveTrialNudge: () => printActiveTrialNudge,
10429
- printGraceNudge: () => printGraceNudge,
10430
- printLicenseBlocked: () => printLicenseBlocked,
10431
- printTrialNudge: () => printTrialNudge,
10432
- promptForLicenseKey: () => promptForLicenseKey,
10433
- promptOpenCheckout: () => promptOpenCheckout,
10434
- resolveUpgradeReason: () => resolveUpgradeReason,
10435
- runUpgradeFlow: () => runUpgradeFlow
10436
- });
10437
- import chalk7 from "chalk";
10438
- function checkoutUrlFor(purpose) {
10439
- return purpose === "upgrade" ? getUpgradeUrl() : getCheckoutUrl();
10440
- }
10441
- function isTrialCutoff(lic) {
10442
- return lic.trialPhase === "expired";
10443
- }
10444
- function isTrialGrace(lic) {
10445
- return lic.trialPhase === "grace";
10406
+ // src/license/lemonsqueezy.ts
10407
+ import { hostname } from "os";
10408
+ function invalid(message) {
10409
+ return {
10410
+ valid: false,
10411
+ edition: "trial",
10412
+ expiresAt: null,
10413
+ message
10414
+ };
10446
10415
  }
10447
- function printLicenseBlocked(context) {
10448
- const lic = checkLicense();
10449
- console.log();
10450
- if (isTrialCutoff(lic)) {
10451
- console.log(" " + chalk7.yellow(randomBlockedNudge()));
10452
- } else {
10453
- console.log(chalk7.red(` A license is required for ${context}.`));
10454
- console.log(
10455
- " " + chalk7.dim("Type ") + paint("accent", "/upgrade") + chalk7.dim(" or ") + paint("accent", "/checkout") + chalk7.dim(" to get a license.")
10456
- );
10457
- }
10458
- console.log();
10416
+ function editionFromMeta(meta) {
10417
+ const label = `${meta?.variant_name ?? ""} ${meta?.product_name ?? ""}`.toLowerCase();
10418
+ if (label.includes("trial")) return "trial";
10419
+ if (label.includes("team")) return "team";
10420
+ return "pro";
10459
10421
  }
10460
- function printGraceNudge(lic) {
10461
- if (!lic.shouldNudgeUpgrade || lic.daysUntilLockout === void 0) return;
10462
- console.log(" " + chalk7.yellow(randomGraceNudge(lic.daysUntilLockout)));
10463
- console.log();
10422
+ function expiresAtFromKey(licenseKey) {
10423
+ if (!licenseKey?.expires_at) return null;
10424
+ const parsed = new Date(licenseKey.expires_at);
10425
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
10464
10426
  }
10465
- function printActiveTrialNudge(lic) {
10466
- if (!lic.shouldNudgeUpgrade || lic.trialPhase !== "active") return;
10467
- const daysLeft = lic.trialDaysRemaining;
10468
- if (daysLeft === void 0 || daysLeft <= 0) return;
10469
- console.log(" " + chalk7.yellow(randomActiveTrialNudge(daysLeft)));
10470
- console.log();
10427
+ function statusMessage(edition, expiresAt, meta) {
10428
+ const product = meta?.variant_name || meta?.product_name;
10429
+ const base = product ? `${edition} license (${product})` : `${edition} license`;
10430
+ return expiresAt ? `${base} (expires ${expiresAt.toISOString().slice(0, 10)})` : base;
10471
10431
  }
10472
- function printTrialNudge(lic) {
10473
- if (!lic.shouldNudgeUpgrade) return;
10474
- if (lic.trialPhase === "grace") {
10475
- printGraceNudge(lic);
10476
- return;
10432
+ function mapLsFailure(error, licenseKey) {
10433
+ const status = licenseKey?.status?.toLowerCase();
10434
+ if (status === "expired") {
10435
+ return invalid("Time's up \u2014 /upgrade and paste your key.");
10477
10436
  }
10478
- if (lic.trialPhase === "active") {
10479
- printActiveTrialNudge(lic);
10437
+ if (status === "disabled") {
10438
+ return invalid("License disabled. Contact support or purchase a new license.");
10439
+ }
10440
+ if (error?.toLowerCase().includes("activation limit")) {
10441
+ return invalid(
10442
+ "License activation limit reached. Deactivate an old machine in your Lemon Squeezy account, then try again."
10443
+ );
10480
10444
  }
10445
+ return invalid(error?.trim() || "Could not activate license key");
10481
10446
  }
10482
- function headlineFor(reason, lic) {
10483
- return randomUpgradeHeadline(lic.daysUntilLockout);
10484
- }
10485
- function subtitleFor(reason) {
10486
- return randomUpgradeSubtitle(reason);
10487
- }
10488
- async function promptOpenCheckout(ctx, purpose = "signup") {
10489
- const url = checkoutUrlFor(purpose);
10490
- console.log(" " + chalk7.dim(url));
10491
- if (!process.stdin.isTTY || process.env.NTRP_NO_BROWSER_OPEN === "1") {
10492
- console.log();
10493
- return;
10494
- }
10495
- const session = createPromptSession(ctx.rl, ctx);
10496
- try {
10497
- console.log();
10498
- await session.askPressEnter("Open checkout in your browser");
10499
- try {
10500
- await openInBrowser(url);
10501
- console.log(" " + chalk7.green("\u2713 Browser opened"));
10502
- console.log(
10503
- " " + chalk7.dim(
10504
- purpose === "upgrade" ? "Complete checkout in your browser. Then paste your Pro key below." : "Complete signup in your browser. Then paste your key below."
10505
- )
10506
- );
10507
- } catch {
10508
- console.log(" " + chalk7.yellow("Could not open the browser. Copy the URL above."));
10509
- }
10510
- console.log();
10511
- } finally {
10512
- session.close();
10447
+ async function postLicense(path, fields) {
10448
+ const body = new URLSearchParams(fields);
10449
+ const res = await fetch(`${LICENSE_API}/${path}`, {
10450
+ method: "POST",
10451
+ headers: { Accept: "application/json" },
10452
+ body,
10453
+ signal: AbortSignal.timeout(15e3)
10454
+ });
10455
+ const data = await res.json();
10456
+ if (!res.ok && !data.error) {
10457
+ throw new Error(`License server error (${res.status})`);
10513
10458
  }
10459
+ return data;
10514
10460
  }
10515
- async function openCheckoutInBrowser() {
10516
- const url = getCheckoutUrl();
10517
- console.log();
10518
- console.log(" " + chalk7.dim(url));
10519
- if (!process.stdin.isTTY) {
10520
- console.log();
10521
- return;
10461
+ function defaultInstanceName() {
10462
+ const host = hostname().replace(/[^\w.-]/g, "-").slice(0, 48) || "machine";
10463
+ const user = (process.env.USER || process.env.USERNAME || "user").replace(/[^\w.-]/g, "-").slice(0, 15);
10464
+ return `ntrp-${host}-${user}`;
10465
+ }
10466
+ async function activateLemonSqueezyLicense(licenseKey, instanceName = defaultInstanceName()) {
10467
+ const data = await postLicense("activate", {
10468
+ license_key: licenseKey,
10469
+ instance_name: instanceName
10470
+ });
10471
+ if (!data.activated) {
10472
+ return mapLsFailure(data.error, data.license_key);
10522
10473
  }
10523
- try {
10524
- await openInBrowser(url);
10525
- console.log(" " + chalk7.green("\u2713 Browser opened"));
10526
- } catch {
10527
- console.log(" " + chalk7.yellow("Could not open the browser. Copy the URL above."));
10474
+ const edition = editionFromMeta(data.meta);
10475
+ const expiresAt = expiresAtFromKey(data.license_key);
10476
+ const instanceId = data.instance?.id;
10477
+ if (!instanceId) {
10478
+ return invalid("Activation succeeded but no instance id was returned. Try again.");
10528
10479
  }
10529
- console.log(" " + chalk7.dim("After signup, paste your key with /activate or /upgrade."));
10530
- console.log();
10480
+ return {
10481
+ valid: true,
10482
+ edition,
10483
+ expiresAt,
10484
+ message: statusMessage(edition, expiresAt, data.meta),
10485
+ instanceId
10486
+ };
10531
10487
  }
10532
- async function promptForLicenseKey(ctx, purpose = "signup") {
10533
- const session = createPromptSession(ctx.rl, ctx);
10534
- try {
10535
- for (; ; ) {
10536
- let key;
10537
- try {
10538
- key = await session.askSecret("Paste your license key", { confirm: false });
10539
- } catch (err) {
10540
- if (err instanceof Error && err.message === "Cancelled") {
10541
- console.log(" " + chalk7.dim("Activation cancelled."));
10542
- return false;
10543
- }
10544
- throw err;
10545
- }
10546
- if (!key.trim()) {
10547
- console.log(" " + chalk7.red("A license key is required."));
10548
- continue;
10549
- }
10550
- let result;
10551
- try {
10552
- result = await activateLicenseKey(key.trim());
10553
- } catch (err) {
10554
- const message = err instanceof Error ? err.message : "License activation failed";
10555
- console.log(" " + chalk7.red(message));
10556
- console.log(" " + chalk7.dim("Check your network connection and try again."));
10557
- console.log();
10558
- continue;
10559
- }
10560
- if (!result.valid) {
10561
- console.log(" " + chalk7.red(result.message));
10562
- console.log(
10563
- " " + chalk7.dim(`Use the key from your purchase email, or try again: ${checkoutUrlFor(purpose)}`)
10564
- );
10565
- console.log();
10566
- continue;
10567
- }
10568
- console.log();
10569
- console.log(chalk7.green(` \u2713 ${randomProActivatedLine()}`));
10570
- console.log();
10571
- return true;
10572
- }
10573
- } finally {
10574
- session.close();
10488
+ async function validateLemonSqueezyLicense(licenseKey, instanceId) {
10489
+ const data = await postLicense("validate", {
10490
+ license_key: licenseKey,
10491
+ instance_id: instanceId
10492
+ });
10493
+ if (!data.valid) {
10494
+ return mapLsFailure(data.error, data.license_key);
10575
10495
  }
10496
+ const edition = editionFromMeta(data.meta);
10497
+ const expiresAt = expiresAtFromKey(data.license_key);
10498
+ return {
10499
+ valid: true,
10500
+ edition,
10501
+ expiresAt,
10502
+ message: statusMessage(edition, expiresAt, data.meta)
10503
+ };
10576
10504
  }
10577
- async function runUpgradeFlow(ctx, reason) {
10578
- const lic = checkLicense();
10579
- printCenteredLogo();
10580
- console.log(" " + bold(headlineFor(reason, lic)));
10581
- console.log(" " + chalk7.dim(subtitleFor(reason)));
10582
- console.log();
10583
- await promptOpenCheckout(ctx, "upgrade");
10584
- console.log(" " + chalk7.dim("Paste your license key when it arrives by email."));
10585
- console.log();
10586
- return promptForLicenseKey(ctx, "upgrade");
10587
- }
10588
- function resolveUpgradeReason() {
10589
- const lic = checkLicense();
10590
- if (isTrialCutoff(lic)) return "expired";
10591
- if (isTrialGrace(lic)) return "grace";
10592
- return "convert";
10593
- }
10594
- function hasStoredLicenseKey() {
10595
- return Boolean(getConfigValue("license-key"));
10596
- }
10597
- var init_upgrade = __esm({
10598
- "src/license/upgrade.ts"() {
10505
+ var LICENSE_API;
10506
+ var init_lemonsqueezy = __esm({
10507
+ "src/license/lemonsqueezy.ts"() {
10599
10508
  "use strict";
10600
- init_prompts();
10601
- init_store();
10602
- init_banner();
10603
- init_theme();
10604
- init_verify();
10605
- init_trial_policy();
10606
- init_upgrade_whimsy();
10607
- init_open_browser();
10509
+ LICENSE_API = "https://api.lemonsqueezy.com/v1/licenses";
10608
10510
  }
10609
10511
  });
10610
10512
 
10611
- // src/license/activation.ts
10612
- import chalk8 from "chalk";
10613
- function hasValidLicense() {
10614
- return checkLicense().valid;
10513
+ // src/license/verify.ts
10514
+ import { createHmac as createHmac2 } from "crypto";
10515
+ function signingSecret() {
10516
+ const secret2 = process.env.NTRP_SIGNING_SECRET;
10517
+ if (!secret2) return null;
10518
+ return secret2;
10615
10519
  }
10616
- async function ensureLicenseActivated(ctx, options = {}) {
10617
- if (hasValidLicense()) return false;
10618
- if (!process.stdin.isTTY) {
10619
- console.error();
10620
- console.error(chalk8.red(" A license key is required."));
10621
- console.error(chalk8.dim(` Sign up: ${getCheckoutUrl()}`));
10622
- console.error(chalk8.dim(" Then type: ntrp activate <key>"));
10623
- console.error(chalk8.dim(" Or set NTRP_LICENSE_KEY for headless use."));
10624
- console.error();
10625
- process.exit(1);
10520
+ function validateLicenseKey(key) {
10521
+ const invalid2 = (msg) => ({
10522
+ valid: false,
10523
+ edition: "trial",
10524
+ expiresAt: null,
10525
+ message: msg
10526
+ });
10527
+ if (!key || !key.startsWith("NTRP-")) {
10528
+ return invalid2("Invalid key format");
10626
10529
  }
10627
- const exitOnCancel = options.exitOnCancel !== false;
10628
- const lic = checkLicense();
10629
- if (hasStoredLicenseKey() && isTrialCutoff(lic)) {
10630
- const upgraded = await runUpgradeFlow(ctx, "expired");
10631
- if (!upgraded) {
10632
- if (exitOnCancel) exitActivationCancelled();
10633
- printActivationCancelledStay();
10634
- return true;
10635
- }
10636
- return true;
10530
+ const secret2 = signingSecret();
10531
+ if (!secret2) {
10532
+ return invalid2("Invalid key format");
10637
10533
  }
10638
- printCenteredLogo();
10639
- console.log(" " + bold("Welcome to NTRP"));
10640
- console.log(" " + chalk8.dim(TAGLINE));
10641
- console.log(" " + chalk8.dim("Paste a trial key or a Pro key. If you do not have a key, NTRP opens signup."));
10642
- console.log(" " + chalk8.dim("Activating accepts the NTRP license (LICENSE in the install, or ntrp.sonnechasser.com)."));
10643
- console.log();
10644
- await promptOpenCheckout(ctx);
10645
- const activated = await promptForLicenseKey(ctx);
10646
- if (!activated) {
10647
- if (exitOnCancel) exitActivationCancelled();
10648
- printActivationCancelledStay();
10649
- return true;
10534
+ const parts = key.replace("NTRP-", "").split("-");
10535
+ if (parts.length !== 3) {
10536
+ return invalid2("Invalid key format");
10650
10537
  }
10651
- return true;
10652
- }
10653
- function exitActivationCancelled() {
10654
- console.log(" " + chalk8.dim("No license activated. Type ") + chalk8.cyan("ntrp") + chalk8.dim(" to try again."));
10655
- console.log();
10656
- process.exit(130);
10538
+ const [payload, meta, signature] = parts;
10539
+ const dataToSign = `${payload}-${meta}`;
10540
+ const expectedSig = createHmac2("sha256", secret2).update(dataToSign).digest("hex").slice(0, 8);
10541
+ if (signature !== expectedSig) {
10542
+ return invalid2("Invalid license key");
10543
+ }
10544
+ const editionCode = meta.slice(0, 2);
10545
+ const expiryHex = meta.slice(2);
10546
+ const edition = editionCode === "01" ? "pro" : editionCode === "02" ? "team" : "trial";
10547
+ const expiryTs = parseInt(expiryHex, 16);
10548
+ const expiresAt = expiryTs > 0 ? new Date(expiryTs * 1e3) : null;
10549
+ if (expiresAt && expiresAt < /* @__PURE__ */ new Date() && edition !== "trial") {
10550
+ return invalid2(`License expired on ${expiresAt.toISOString().slice(0, 10)}`);
10551
+ }
10552
+ return {
10553
+ valid: true,
10554
+ edition,
10555
+ expiresAt,
10556
+ message: edition === "trial" ? "trial license" : `${edition} license${expiresAt ? ` (expires ${expiresAt.toISOString().slice(0, 10)})` : " (perpetual)"}`
10557
+ };
10657
10558
  }
10658
- function printActivationCancelledStay() {
10659
- console.log(" " + chalk8.dim("No license activated. Type ") + chalk8.cyan("/checkout") + chalk8.dim(" to continue."));
10660
- console.log();
10559
+ function trialActivatedAt() {
10560
+ const stored = getConfigValue("license-activated-at");
10561
+ if (stored) {
10562
+ const parsed = new Date(stored);
10563
+ if (!Number.isNaN(parsed.getTime())) return parsed;
10564
+ }
10565
+ const now2 = /* @__PURE__ */ new Date();
10566
+ setConfigValue("license-activated-at", now2.toISOString());
10567
+ return now2;
10661
10568
  }
10662
- var init_activation = __esm({
10663
- "src/license/activation.ts"() {
10664
- "use strict";
10665
- init_banner();
10666
- init_theme();
10667
- init_verify();
10668
- init_trial_policy();
10669
- init_upgrade();
10569
+ function recordLicenseActivation(edition) {
10570
+ if (edition === "trial") {
10571
+ setConfigValue("license-activated-at", (/* @__PURE__ */ new Date()).toISOString());
10572
+ } else {
10573
+ deleteConfigValue("license-activated-at");
10670
10574
  }
10671
- });
10672
-
10673
- // src/demo/scenarios.ts
10674
- var scenarios_exports = {};
10675
- __export(scenarios_exports, {
10676
- NAMED_DEMO_SCENARIOS: () => NAMED_DEMO_SCENARIOS,
10677
- SCENARIOS: () => SCENARIOS,
10678
- SCENARIO_LIST: () => SCENARIO_LIST,
10679
- blendScenarios: () => blendScenarios,
10680
- getScenario: () => getScenario,
10681
- isNamedDemoScenario: () => isNamedDemoScenario,
10682
- pickRandomScenario: () => pickRandomScenario,
10683
- resolveScenarioInput: () => resolveScenarioInput
10684
- });
10685
- function getScenario(key) {
10686
- const scenario = SCENARIOS[key];
10687
- if (!scenario) {
10688
- throw new Error(`Unknown scenario: ${key}. Valid: ${Object.keys(SCENARIOS).join(", ")}`);
10575
+ }
10576
+ function applyTrialPolicy(result) {
10577
+ if (result.edition !== "trial") return result;
10578
+ const trial = evaluateTrial(trialActivatedAt());
10579
+ if (trial.phase === "expired") {
10580
+ return {
10581
+ valid: false,
10582
+ edition: "trial",
10583
+ expiresAt: result.expiresAt,
10584
+ message: randomCutoffNudge(),
10585
+ trialPhase: "expired",
10586
+ shouldNudgeUpgrade: false,
10587
+ daysUntilLockout: 0,
10588
+ trialDaysRemaining: 0
10589
+ };
10689
10590
  }
10690
- return scenario;
10591
+ const message = trial.phase === "grace" ? `trial license (grace: ${trial.daysUntilLockout} day${trial.daysUntilLockout === 1 ? "" : "s"} left)` : formatTrialActiveMessage(trial.daysSinceActivation);
10592
+ return {
10593
+ ...result,
10594
+ message,
10595
+ trialPhase: trial.phase,
10596
+ shouldNudgeUpgrade: trial.shouldNudge,
10597
+ daysUntilLockout: trial.daysUntilLockout,
10598
+ trialDaysRemaining: trial.trialDaysRemaining
10599
+ };
10691
10600
  }
10692
- function isNamedDemoScenario(raw) {
10693
- return NAMED_DEMO_SCENARIOS.includes(raw);
10601
+ function storedLicenseProvider(key) {
10602
+ const configured = getConfigValue("license-provider");
10603
+ if (configured === "ntrp" || configured === "lemonsqueezy") return configured;
10604
+ return detectLicenseFormat(key) === "lemonsqueezy" ? "lemonsqueezy" : "ntrp";
10694
10605
  }
10695
- function resolveScenarioInput(raw) {
10696
- const input = raw?.trim();
10697
- if (!input) return void 0;
10698
- if (input === "research_blend") return "research_blend";
10699
- if (NAMED_DEMO_SCENARIOS.includes(input)) {
10700
- return input;
10606
+ function checkLemonSqueezyLicense(key) {
10607
+ const instanceId = getConfigValue("license-instance-id");
10608
+ if (!instanceId) {
10609
+ return {
10610
+ valid: false,
10611
+ edition: "trial",
10612
+ expiresAt: null,
10613
+ message: "License not activated on this machine. Run: ntrp activate <key>"
10614
+ };
10701
10615
  }
10702
- const n = Number(input);
10703
- if (Number.isInteger(n) && n >= 1 && n <= NAMED_DEMO_SCENARIOS.length) {
10704
- return NAMED_DEMO_SCENARIOS[n - 1];
10616
+ const edition = getConfigValue("license-edition") ?? "pro";
10617
+ const base = {
10618
+ valid: true,
10619
+ edition,
10620
+ expiresAt: null,
10621
+ message: `${edition} license`
10622
+ };
10623
+ return applyTrialPolicy(base);
10624
+ }
10625
+ async function activateLicenseKey(rawKey) {
10626
+ const key = normalizeLicenseKeyInput(rawKey);
10627
+ const format = detectLicenseFormat(key);
10628
+ if (format === "unknown") {
10629
+ return {
10630
+ valid: false,
10631
+ edition: "trial",
10632
+ expiresAt: null,
10633
+ message: "Invalid key format"
10634
+ };
10705
10635
  }
10706
- return null;
10636
+ if (format === "ntrp") {
10637
+ const result = validateLicenseKey(key);
10638
+ if (!result.valid) return result;
10639
+ setConfigValue("license-key", key);
10640
+ setConfigValue("license-provider", "ntrp");
10641
+ deleteConfigValue("license-instance-id");
10642
+ deleteConfigValue("license-edition");
10643
+ recordLicenseActivation(result.edition);
10644
+ return checkLicense();
10645
+ }
10646
+ const activated = await activateLemonSqueezyLicense(key);
10647
+ if (!activated.valid || !activated.instanceId) return activated;
10648
+ setConfigValue("license-key", key);
10649
+ setConfigValue("license-provider", "lemonsqueezy");
10650
+ setConfigValue("license-instance-id", activated.instanceId);
10651
+ setConfigValue("license-edition", activated.edition);
10652
+ recordLicenseActivation(activated.edition);
10653
+ return checkLicense();
10707
10654
  }
10708
- function pickRandomScenario() {
10709
- return RANDOM_POOL[Math.floor(Math.random() * RANDOM_POOL.length)];
10655
+ async function refreshLicenseOnline() {
10656
+ const key = getConfigValue("license-key");
10657
+ if (!key || storedLicenseProvider(key) !== "lemonsqueezy") {
10658
+ return checkLicense();
10659
+ }
10660
+ const instanceId = getConfigValue("license-instance-id");
10661
+ if (!instanceId) return checkLicense();
10662
+ try {
10663
+ const result = await validateLemonSqueezyLicense(key, instanceId);
10664
+ if (!result.valid) return result;
10665
+ setConfigValue("license-edition", result.edition);
10666
+ return checkLicense();
10667
+ } catch {
10668
+ return checkLicense();
10669
+ }
10710
10670
  }
10711
- function blendScenarios(_research) {
10712
- const blended = {
10713
- ...BASELINE,
10714
- key: "research_blend",
10715
- label: "Research-Derived Blend",
10716
- description: "Realistic data with mild-to-moderate problems across all vital signs.",
10717
- story: "Generated with default research blend. Problems are seeded across all vital signs at realistic levels.",
10718
- hook: "Mild-to-moderate problems seeded across all five vitals.",
10719
- // Bump all problems slightly above baseline for discoverability
10720
- staleContactRatio: 0.2,
10721
- pastCloseDateRatio: 0.15,
10722
- mqlDropRatio: 0.15,
10723
- qualifiedNoOutreachRatio: 0.15,
10724
- stuckDealRatio: 0.15,
10725
- noiseActivityRatio: 0.2,
10726
- singleThreadRatio: 0.25
10727
- };
10728
- return blended;
10671
+ function checkLicense() {
10672
+ const key = getConfigValue("license-key");
10673
+ if (!key) {
10674
+ return {
10675
+ valid: false,
10676
+ edition: "trial",
10677
+ expiresAt: null,
10678
+ message: "No license key found. Run: ntrp activate <key>"
10679
+ };
10680
+ }
10681
+ if (storedLicenseProvider(key) === "lemonsqueezy") {
10682
+ return checkLemonSqueezyLicense(key);
10683
+ }
10684
+ const result = validateLicenseKey(key);
10685
+ if (!result.valid || result.edition !== "trial") {
10686
+ return result;
10687
+ }
10688
+ return applyTrialPolicy(result);
10729
10689
  }
10730
- var BASELINE, SCENARIOS, SCENARIO_LIST, NAMED_DEMO_SCENARIOS, RANDOM_POOL;
10731
- var init_scenarios = __esm({
10732
- "src/demo/scenarios.ts"() {
10690
+ var init_verify = __esm({
10691
+ "src/license/verify.ts"() {
10733
10692
  "use strict";
10734
- BASELINE = {
10735
- enterpriseRatio: 0.2,
10736
- midMarketRatio: 0.3,
10737
- smbRatio: 0.5,
10738
- staleContactRatio: 0.15,
10739
- staleContactRatioEnterprise: 0.2,
10740
- staleContactRatioSmb: 0.1,
10741
- pastCloseDateRatio: 0.1,
10742
- staleDays: 120,
10743
- mqlDropRatio: 0.1,
10744
- qualifiedNoOutreachRatio: 0.1,
10745
- stuckDealRatio: 0.1,
10746
- stuckInNegotiationDays: 45,
10747
- avgDaysPerStageEnterprise: 25,
10748
- avgDaysPerStageSmb: 8,
10749
- activityVolumeMultiplier: 1,
10750
- noiseActivityRatio: 0.15,
10751
- singleThreadRatio: 0.2,
10752
- loneWolfRepIndex: null,
10753
- loneWolfSingleThreadRatio: 0,
10754
- freshnessGapDays: 90
10755
- };
10756
- SCENARIOS = {
10757
- hidden_crisis: {
10758
- ...BASELINE,
10759
- key: "hidden_crisis",
10760
- label: "The Hidden Crisis",
10761
- description: "Overall health looks yellow but Enterprise is deep red, masked by strong SMB numbers.",
10762
- story: "Your aggregate numbers look okay \u2014 but when you break it by segment, Enterprise is dying. 60% of enterprise contacts have gone dark, deals are single-threaded, and SMB is carrying the average.",
10763
- hook: "SMB is carrying the average while Enterprise dies quietly.",
10764
- staleContactRatio: 0.3,
10765
- staleContactRatioEnterprise: 0.6,
10766
- staleContactRatioSmb: 0.1,
10767
- singleThreadRatio: 0.5,
10768
- enterpriseRatio: 0.3,
10769
- midMarketRatio: 0.3,
10770
- smbRatio: 0.4
10771
- },
10772
- leaky_bucket: {
10773
- ...BASELINE,
10774
- key: "leaky_bucket",
10775
- label: "The Leaky Bucket",
10776
- description: "Marketing generates plenty of leads but 40% vanish at handoff to sales.",
10777
- story: "Marketing is doing its job \u2014 MQLs are flowing. But 40% of qualified leads never show up in sales workflows. They're falling through the cracks at handoff, and nobody's noticing because marketing reports MQL count and sales reports pipeline value.",
10778
- hook: "MQLs flow in, then 40% vanish at the sales handoff.",
10779
- mqlDropRatio: 0.4,
10780
- qualifiedNoOutreachRatio: 0.35,
10781
- staleContactRatio: 0.2
10782
- },
10783
- stale_pipeline: {
10784
- ...BASELINE,
10785
- key: "stale_pipeline",
10786
- label: "The Stale Pipeline",
10787
- description: "Big pipeline number but half the deals are zombies stuck in late stages.",
10788
- story: "The pipeline report says $5M. But look closer: half those deals have close dates in the past, 40% are stuck in Negotiation for 120+ days, and nobody's touching them. You're forecasting on fiction.",
10789
- hook: "Half the pipeline is zombies \u2014 you're forecasting on fiction.",
10790
- pastCloseDateRatio: 0.5,
10791
- stuckDealRatio: 0.4,
10792
- stuckInNegotiationDays: 120,
10793
- staleContactRatio: 0.25,
10794
- staleDays: 90
10795
- },
10796
- lone_wolf: {
10797
- ...BASELINE,
10798
- key: "lone_wolf",
10799
- label: "The Lone Wolf",
10800
- description: "One rep has great numbers but every single deal is single-threaded.",
10801
- story: "Your top rep is crushing it on paper \u2014 biggest pipeline, highest close rate. But every deal has exactly one contact. One champion goes on vacation, gets promoted, or leaves, and the entire pipeline collapses.",
10802
- hook: "Top rep, huge pipeline, one contact per deal \u2014 one exit from collapse.",
10803
- loneWolfRepIndex: 0,
10804
- loneWolfSingleThreadRatio: 1,
10805
- singleThreadRatio: 0.15
10806
- },
10807
- busy_bees: {
10808
- ...BASELINE,
10809
- key: "busy_bees",
10810
- label: "The Busy Bees",
10811
- description: "High activity volume across the team, but most of it hits dead ends.",
10812
- story: "Your team is busy. Activity metrics look great \u2014 calls are up, emails are up, meetings are up. But 60% of that activity is aimed at contacts with no associated pipeline. Reps are spraying, not aiming.",
10813
- hook: "Reps are spraying, not aiming.",
10814
- activityVolumeMultiplier: 3,
10815
- noiseActivityRatio: 0.6,
10816
- staleContactRatio: 0.2
10817
- },
10818
- even_keel: {
10819
- ...BASELINE,
10820
- key: "even_keel",
10821
- label: "The Even Keel",
10822
- description: "A reasonably healthy book \u2014 enough yellow to listen, not a five-alarm fire.",
10823
- story: "Most numbers sit in a normal band. A few contacts have gone quiet, a handful of deals are slow, activity is mostly on-pipeline. This is what 'fine' looks like on the stethoscope \u2014 useful when you want to evaluate NTRP without a manufactured crisis.",
10824
- hook: "Reasonably healthy \u2014 enough signal to listen, not a crisis."
10825
- },
10826
- compound_pain: {
10827
- ...BASELINE,
10828
- key: "compound_pain",
10829
- label: "The Compound Fracture",
10830
- description: "Several vitals are red at once \u2014 stale pipeline, leaky handoff, noisy activity, thin threads.",
10831
- story: "This isn't one problem. Enterprise contacts have gone dark, MQLs vanish at handoff, late-stage deals are zombies, and a lot of activity never touches pipeline. The gating logic has to pick a first red \u2014 that's the point of this book.",
10832
- hook: "Several vitals red at once \u2014 the stethoscope has to pick a first listen.",
10833
- enterpriseRatio: 0.3,
10834
- midMarketRatio: 0.3,
10835
- smbRatio: 0.4,
10836
- staleContactRatio: 0.35,
10837
- staleContactRatioEnterprise: 0.55,
10838
- staleContactRatioSmb: 0.15,
10839
- pastCloseDateRatio: 0.35,
10840
- staleDays: 100,
10841
- mqlDropRatio: 0.3,
10842
- qualifiedNoOutreachRatio: 0.25,
10843
- stuckDealRatio: 0.3,
10844
- stuckInNegotiationDays: 90,
10845
- activityVolumeMultiplier: 2,
10846
- noiseActivityRatio: 0.4,
10847
- singleThreadRatio: 0.4
10848
- }
10849
- };
10850
- SCENARIO_LIST = Object.values(SCENARIOS);
10851
- NAMED_DEMO_SCENARIOS = [
10852
- "hidden_crisis",
10853
- "leaky_bucket",
10854
- "stale_pipeline",
10855
- "lone_wolf",
10856
- "busy_bees",
10857
- "even_keel",
10858
- "compound_pain"
10859
- ];
10860
- RANDOM_POOL = [...NAMED_DEMO_SCENARIOS];
10693
+ init_store();
10694
+ init_trial_policy();
10695
+ init_upgrade_whimsy();
10696
+ init_normalize();
10697
+ init_lemonsqueezy();
10861
10698
  }
10862
10699
  });
10863
10700
 
10864
- // src/baselines/metrics-benchmarks.ts
10865
- function resolveMetricBenchmarks(motion) {
10866
- return METRICS_BENCHMARKS[motion ?? "mid_market"];
10867
- }
10868
- function motionBenchmarkLabel(motion) {
10869
- return MOTION_LABELS[motion ?? "mid_market"];
10870
- }
10871
- function metricStatusHigherIsBetter(value, threshold) {
10872
- if (value >= threshold.green) return "green";
10873
- if (value >= threshold.yellow) return "yellow";
10874
- return "red";
10701
+ // src/ui/open-browser.ts
10702
+ import { spawn } from "child_process";
10703
+ import { platform } from "os";
10704
+ function openInBrowser(url) {
10705
+ return new Promise((resolve10, reject) => {
10706
+ let cmd;
10707
+ let args;
10708
+ switch (platform()) {
10709
+ case "darwin":
10710
+ cmd = "open";
10711
+ args = [url];
10712
+ break;
10713
+ case "win32":
10714
+ cmd = "cmd";
10715
+ args = ["/c", "start", "", url];
10716
+ break;
10717
+ default:
10718
+ cmd = "xdg-open";
10719
+ args = [url];
10720
+ break;
10721
+ }
10722
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
10723
+ child.on("error", reject);
10724
+ child.unref();
10725
+ resolve10();
10726
+ });
10875
10727
  }
10876
- var METRICS_BENCHMARKS, MOTION_LABELS;
10877
- var init_metrics_benchmarks = __esm({
10878
- "src/baselines/metrics-benchmarks.ts"() {
10728
+ var init_open_browser = __esm({
10729
+ "src/ui/open-browser.ts"() {
10879
10730
  "use strict";
10880
- METRICS_BENCHMARKS = {
10881
- plg: {
10882
- nrr: { green: 110, yellow: 100 },
10883
- grr: { green: 85, yellow: 75 },
10884
- win_rate: { green: 25, yellow: 15 },
10885
- pipeline_coverage: { green: 4, yellow: 2.5 },
10886
- magic_number: { green: 1, yellow: 0.75 },
10887
- payback_months: { green: 12, yellow: 18 }
10888
- },
10889
- smb_velocity: {
10890
- nrr: { green: 105, yellow: 95 },
10891
- grr: { green: 88, yellow: 78 },
10892
- win_rate: { green: 22, yellow: 12 },
10893
- pipeline_coverage: { green: 3.5, yellow: 2 },
10894
- magic_number: { green: 0.9, yellow: 0.6 },
10895
- payback_months: { green: 14, yellow: 20 }
10896
- },
10897
- mid_market: {
10898
- nrr: { green: 100, yellow: 90 },
10899
- grr: { green: 90, yellow: 80 },
10900
- win_rate: { green: 20, yellow: 12 },
10901
- pipeline_coverage: { green: 3, yellow: 2 },
10902
- magic_number: { green: 0.75, yellow: 0.5 },
10903
- payback_months: { green: 16, yellow: 22 }
10904
- },
10905
- enterprise: {
10906
- nrr: { green: 95, yellow: 85 },
10907
- grr: { green: 92, yellow: 82 },
10908
- win_rate: { green: 15, yellow: 8 },
10909
- pipeline_coverage: { green: 2.5, yellow: 1.5 },
10910
- magic_number: { green: 0.6, yellow: 0.4 },
10911
- payback_months: { green: 18, yellow: 24 }
10912
- }
10913
- };
10914
- MOTION_LABELS = {
10915
- plg: "PLG",
10916
- smb_velocity: "SMB Velocity",
10917
- mid_market: "Mid-Market",
10918
- enterprise: "Enterprise"
10919
- };
10920
10731
  }
10921
10732
  });
10922
10733
 
10923
- // src/data/metric-definitions.ts
10924
- function pctBand(metric, motion) {
10925
- const m = motion ?? "mid_market";
10926
- const t = METRICS_BENCHMARKS[m][metric];
10927
- return `${motionBenchmarkLabel(m)} green \u2265${t.green}${metric === "pipeline_coverage" ? "x" : "%"}, yellow \u2265${t.yellow}${metric === "pipeline_coverage" ? "x" : "%"}`;
10734
+ // src/license/upgrade.ts
10735
+ var upgrade_exports = {};
10736
+ __export(upgrade_exports, {
10737
+ getCheckoutUrl: () => getCheckoutUrl,
10738
+ getUpgradeUrl: () => getUpgradeUrl,
10739
+ hasStoredLicenseKey: () => hasStoredLicenseKey,
10740
+ isTrialCutoff: () => isTrialCutoff,
10741
+ isTrialGrace: () => isTrialGrace,
10742
+ openCheckoutInBrowser: () => openCheckoutInBrowser,
10743
+ printActiveTrialNudge: () => printActiveTrialNudge,
10744
+ printGraceNudge: () => printGraceNudge,
10745
+ printLicenseBlocked: () => printLicenseBlocked,
10746
+ printTrialNudge: () => printTrialNudge,
10747
+ promptForLicenseKey: () => promptForLicenseKey,
10748
+ promptOpenCheckout: () => promptOpenCheckout,
10749
+ resolveUpgradeReason: () => resolveUpgradeReason,
10750
+ runUpgradeFlow: () => runUpgradeFlow
10751
+ });
10752
+ import chalk8 from "chalk";
10753
+ function checkoutUrlFor(purpose) {
10754
+ return purpose === "upgrade" ? getUpgradeUrl() : getCheckoutUrl();
10928
10755
  }
10929
- function monthsBand(motion) {
10930
- const m = motion ?? "mid_market";
10931
- const t = METRICS_BENCHMARKS[m].payback_months;
10932
- return `${motionBenchmarkLabel(m)} green \u2264${t.green}mo, yellow \u2264${t.yellow}mo`;
10756
+ function isTrialCutoff(lic) {
10757
+ return lic.trialPhase === "expired";
10933
10758
  }
10934
- function magicBand(motion) {
10935
- const m = motion ?? "mid_market";
10936
- const t = METRICS_BENCHMARKS[m].magic_number;
10937
- return `${motionBenchmarkLabel(m)} green \u2265${t.green}, yellow \u2265${t.yellow}`;
10759
+ function isTrialGrace(lic) {
10760
+ return lic.trialPhase === "grace";
10938
10761
  }
10939
- function getMetricExplainer(id) {
10940
- return BY_ID.get(id);
10762
+ function printLicenseBlocked(context) {
10763
+ const lic = checkLicense();
10764
+ console.log();
10765
+ if (isTrialCutoff(lic)) {
10766
+ console.log(" " + chalk8.yellow(randomBlockedNudge()));
10767
+ } else {
10768
+ console.log(chalk8.red(` A license is required for ${context}.`));
10769
+ console.log(
10770
+ " " + chalk8.dim("Type ") + paint("accent", "/activate") + chalk8.dim(" to paste a key, or ") + paint("accent", "/checkout") + chalk8.dim(" to sign up.")
10771
+ );
10772
+ }
10773
+ console.log();
10941
10774
  }
10942
- function resolveMetricId(query) {
10943
- const q = query.trim().toLowerCase().replace(/\s+/g, " ");
10944
- if (!q) return void 0;
10945
- if (BY_ID.has(q)) return q;
10946
- const direct = ALIAS_INDEX.get(q);
10947
- if (direct) return direct;
10948
- const norm = q.replace(/[-\s]+/g, "_");
10949
- if (BY_ID.has(norm)) return norm;
10950
- return ALIAS_INDEX.get(norm);
10775
+ function printGraceNudge(lic) {
10776
+ if (!lic.shouldNudgeUpgrade || lic.daysUntilLockout === void 0) return;
10777
+ console.log(" " + chalk8.yellow(randomGraceNudge(lic.daysUntilLockout)));
10778
+ console.log();
10951
10779
  }
10952
- function listMetricExplainers(kind) {
10953
- if (!kind) return METRIC_DEFINITIONS.slice();
10954
- return METRIC_DEFINITIONS.filter((m) => m.kind === kind);
10780
+ function printActiveTrialNudge(lic) {
10781
+ if (!lic.shouldNudgeUpgrade || lic.trialPhase !== "active") return;
10782
+ const daysLeft = lic.trialDaysRemaining;
10783
+ if (daysLeft === void 0 || daysLeft <= 0) return;
10784
+ console.log(" " + chalk8.yellow(randomActiveTrialNudge(daysLeft)));
10785
+ console.log();
10955
10786
  }
10956
- function getCoreDeckExplainers() {
10957
- return CORE_DECK_IDS.map((id) => BY_ID.get(id)).filter(Boolean);
10787
+ function printTrialNudge(lic) {
10788
+ if (!lic.shouldNudgeUpgrade) return;
10789
+ if (lic.trialPhase === "grace") {
10790
+ printGraceNudge(lic);
10791
+ return;
10792
+ }
10793
+ if (lic.trialPhase === "active") {
10794
+ printActiveTrialNudge(lic);
10795
+ }
10958
10796
  }
10959
- var VITALS, SAAS, METRIC_DEFINITIONS, CORE_DECK_IDS, BY_ID, ALIAS_INDEX, SAAS_METRIC_IDS, GATING_LAYER_VISUAL;
10960
- var init_metric_definitions = __esm({
10961
- "src/data/metric-definitions.ts"() {
10962
- "use strict";
10963
- init_metrics_benchmarks();
10964
- VITALS = [
10965
- {
10966
- id: "freshness",
10967
- kind: "vital",
10968
- label: "Freshness",
10969
- group: "Vital Signs",
10970
- tagline: "Does the CRM report which records are still active?",
10971
- how_computed: "NTRP computes a weighted average of people, organizations, and opportunities with recent activity. Open opportunities must also not be past-due. Default windows: people and organizations 90 days, opportunities 30 days. Weights are 35 / 30 / 35.",
10972
- formula_lines: [
10973
- "freshness = people%\xD70.35 + orgs%\xD70.30 + opps%\xD70.35",
10974
- "people/orgs fresh if activity within 90d",
10975
- "opps fresh if activity within 30d AND not past-due"
10976
- ],
10977
- meaning: "Board question: how much of this pipeline is real versus fiction? Dollar value equals the sum of amount on stale opportunities: pipeline at risk.",
10978
- expert_read: "Cut by owner and by stage first. Freshness reds concentrate on people or process. They rarely spread evenly. In a long-cycle enterprise motion, 30 quiet days can be normal cadence. In a velocity motion, 30 quiet days is a dead deal. A sudden cliff usually means a broken integration or a departed rep. It is not gradual decay. Check this false positive: bulk-imported records that nobody has touched yet.",
10979
- deepdive: [
10980
- "Status bands: green 80 or more, yellow 60 or more, red below 60. Motion presets can change the windows.",
10981
- 'Dollar translation: sum of amount on stale open opportunities \u2192 "pipeline at risk".',
10982
- "Layer 1 of the gating stack. A red score here limits trust in later layers.",
10983
- "Trigger play: Clean Dead Pipeline (clean-dead-pipeline) when the score is below 60.",
10984
- "Levers: stale-deal alert at N quiet days. Weekly hygiene scrub. Enrichment refresh on quiet records. Signal-triggered reactivation for paid dormant accounts."
10985
- ],
10986
- visual: {
10987
- kind: "bars",
10988
- caption: "Example component mix. Higher bars are fresher.",
10989
- bars: [
10990
- { label: "People", value: 72, tone: "yellow" },
10991
- { label: "Organizations", value: 81, tone: "green" },
10992
- { label: "Opportunities", value: 44, tone: "red" }
10993
- ]
10994
- },
10995
- play_id: "clean-dead-pipeline",
10996
- dollar_label: "pipeline at risk",
10997
- audience: {
10998
- board: "Freshness answers whether the pipeline number is real. Low freshness means forecast risk. Stale deals inflate coverage and hide the true gap.",
10999
- ops: "Score equals weighted recency across people, organizations, and opportunities. Cut by owner and stage. Set a stale-deal alert and a weekly scrub. Play: Clean Dead Pipeline."
11000
- },
11001
- aliases: ["data freshness", "stale", "zombie deals", "crm freshness"]
11002
- },
11003
- {
11004
- id: "flow_rate",
11005
- kind: "vital",
11006
- label: "Flow Rate",
11007
- group: "Vital Signs",
11008
- tagline: "How fast do deals move, and where do they stop?",
11009
- how_computed: "NTRP sets a base score from average open-deal age versus max_days. It then applies a penalty of up to 20 for the share of stuck deals. A deal is stuck when it has no update beyond stuck_days, or a past-due close. Status uses average open age, not the score alone.",
11010
- formula_lines: [
11011
- "base = 100 \xD7 (1 \u2212 avgOpenAge / max_days)",
11012
- "score = base \u2212 stuckSharePenalty (\u226420)",
11013
- "stuck = no update > stuck_days OR past-due close"
11014
- ],
10797
+ function headlineFor(reason, lic) {
10798
+ return randomUpgradeHeadline(lic.daysUntilLockout);
10799
+ }
10800
+ function subtitleFor(reason) {
10801
+ return randomUpgradeSubtitle(reason);
10802
+ }
10803
+ async function promptOpenCheckout(ctx, purpose = "signup") {
10804
+ const url = checkoutUrlFor(purpose);
10805
+ console.log(" " + chalk8.dim(url));
10806
+ if (!process.stdin.isTTY || process.env.NTRP_NO_BROWSER_OPEN === "1") {
10807
+ console.log();
10808
+ return;
10809
+ }
10810
+ const session = createPromptSession(ctx.rl, ctx);
10811
+ try {
10812
+ console.log();
10813
+ await session.askPressEnter("Open checkout in your browser");
10814
+ try {
10815
+ await openInBrowser(url);
10816
+ console.log(" " + chalk8.green("\u2713 Browser opened"));
10817
+ console.log(
10818
+ " " + chalk8.dim(
10819
+ purpose === "upgrade" ? "Complete checkout in your browser. Then paste your Pro key below." : "Complete signup in your browser. Then paste your key below."
10820
+ )
10821
+ );
10822
+ } catch {
10823
+ console.log(" " + chalk8.yellow("Could not open the browser. Copy the URL above."));
10824
+ }
10825
+ console.log();
10826
+ } finally {
10827
+ session.close();
10828
+ }
10829
+ }
10830
+ async function openCheckoutInBrowser() {
10831
+ const url = getCheckoutUrl();
10832
+ console.log();
10833
+ console.log(" " + chalk8.dim(url));
10834
+ if (!process.stdin.isTTY) {
10835
+ console.log();
10836
+ return;
10837
+ }
10838
+ try {
10839
+ await openInBrowser(url);
10840
+ console.log(" " + chalk8.green("\u2713 Browser opened"));
10841
+ } catch {
10842
+ console.log(" " + chalk8.yellow("Could not open the browser. Copy the URL above."));
10843
+ }
10844
+ console.log(" " + chalk8.dim("After signup, paste your key with /activate or /upgrade."));
10845
+ console.log();
10846
+ }
10847
+ async function promptForLicenseKey(ctx, purpose = "signup") {
10848
+ const session = createPromptSession(ctx.rl, ctx);
10849
+ try {
10850
+ for (; ; ) {
10851
+ let key;
10852
+ try {
10853
+ key = await session.askSecret("Paste your license key", { confirm: false });
10854
+ } catch (err) {
10855
+ if (err instanceof Error && err.message === "Cancelled") {
10856
+ console.log(" " + chalk8.dim("Activation cancelled."));
10857
+ return false;
10858
+ }
10859
+ throw err;
10860
+ }
10861
+ if (!key.trim()) {
10862
+ console.log(" " + chalk8.red("A license key is required."));
10863
+ continue;
10864
+ }
10865
+ let result;
10866
+ try {
10867
+ result = await activateLicenseKey(key.trim());
10868
+ } catch (err) {
10869
+ const message = err instanceof Error ? err.message : "License activation failed";
10870
+ console.log(" " + chalk8.red(message));
10871
+ console.log(" " + chalk8.dim("Check your network connection and try again."));
10872
+ console.log();
10873
+ continue;
10874
+ }
10875
+ if (!result.valid) {
10876
+ console.log(" " + chalk8.red(result.message));
10877
+ console.log(
10878
+ " " + chalk8.dim(`Use the key from your purchase email, or try again: ${checkoutUrlFor(purpose)}`)
10879
+ );
10880
+ console.log();
10881
+ continue;
10882
+ }
10883
+ console.log();
10884
+ console.log(chalk8.green(` \u2713 ${randomProActivatedLine()}`));
10885
+ console.log();
10886
+ return true;
10887
+ }
10888
+ } finally {
10889
+ session.close();
10890
+ }
10891
+ }
10892
+ async function runUpgradeFlow(ctx, reason) {
10893
+ const lic = checkLicense();
10894
+ printCenteredLogo();
10895
+ console.log(" " + bold(headlineFor(reason, lic)));
10896
+ console.log(" " + chalk8.dim(subtitleFor(reason)));
10897
+ console.log();
10898
+ await promptOpenCheckout(ctx, "upgrade");
10899
+ console.log(" " + chalk8.dim("Paste your license key when it arrives by email."));
10900
+ console.log();
10901
+ return promptForLicenseKey(ctx, "upgrade");
10902
+ }
10903
+ function resolveUpgradeReason() {
10904
+ const lic = checkLicense();
10905
+ if (isTrialCutoff(lic)) return "expired";
10906
+ if (isTrialGrace(lic)) return "grace";
10907
+ return "convert";
10908
+ }
10909
+ function hasStoredLicenseKey() {
10910
+ return Boolean(getConfigValue("license-key"));
10911
+ }
10912
+ var init_upgrade = __esm({
10913
+ "src/license/upgrade.ts"() {
10914
+ "use strict";
10915
+ init_prompts();
10916
+ init_store();
10917
+ init_banner();
10918
+ init_theme();
10919
+ init_verify();
10920
+ init_trial_policy();
10921
+ init_upgrade_whimsy();
10922
+ init_open_browser();
10923
+ }
10924
+ });
10925
+
10926
+ // src/license/activation.ts
10927
+ import chalk9 from "chalk";
10928
+ function hasValidLicense() {
10929
+ return checkLicense().valid;
10930
+ }
10931
+ async function ensureLicenseActivated(ctx, options = {}) {
10932
+ if (hasValidLicense()) return false;
10933
+ if (!process.stdin.isTTY) {
10934
+ console.error();
10935
+ console.error(chalk9.red(" A license key is required."));
10936
+ console.error(chalk9.dim(` Sign up: ${getCheckoutUrl()}`));
10937
+ console.error(chalk9.dim(" Then type: ntrp activate <key>"));
10938
+ console.error(chalk9.dim(" Or set NTRP_LICENSE_KEY for headless use."));
10939
+ console.error();
10940
+ process.exit(1);
10941
+ }
10942
+ const exitOnCancel = options.exitOnCancel !== false;
10943
+ const lic = checkLicense();
10944
+ if (hasStoredLicenseKey() && isTrialCutoff(lic)) {
10945
+ const upgraded = await runUpgradeFlow(ctx, "expired");
10946
+ if (!upgraded) {
10947
+ if (exitOnCancel) exitActivationCancelled();
10948
+ printActivationCancelledStay();
10949
+ return true;
10950
+ }
10951
+ return true;
10952
+ }
10953
+ printCenteredLogo();
10954
+ console.log(" " + bold("Welcome to NTRP"));
10955
+ console.log(" " + chalk9.dim(TAGLINE));
10956
+ console.log(" " + chalk9.dim("Paste a trial key or a Pro key. If you do not have a key, NTRP opens signup."));
10957
+ console.log(" " + chalk9.dim("Activating accepts the NTRP license (LICENSE in the install, or ntrp.sonnechasser.com)."));
10958
+ console.log();
10959
+ await promptOpenCheckout(ctx);
10960
+ const activated = await promptForLicenseKey(ctx);
10961
+ if (!activated) {
10962
+ if (exitOnCancel) exitActivationCancelled();
10963
+ printActivationCancelledStay();
10964
+ return true;
10965
+ }
10966
+ return true;
10967
+ }
10968
+ function exitActivationCancelled() {
10969
+ console.log(" " + chalk9.dim("No license activated. Type ") + chalk9.cyan("ntrp") + chalk9.dim(" to try again."));
10970
+ console.log();
10971
+ process.exit(130);
10972
+ }
10973
+ function printActivationCancelledStay() {
10974
+ console.log(
10975
+ " " + chalk9.dim("No license activated. Type ") + chalk9.cyan("/activate") + chalk9.dim(" to paste a key. Type ") + chalk9.cyan("/checkout") + chalk9.dim(" if you need to sign up.")
10976
+ );
10977
+ console.log();
10978
+ }
10979
+ var init_activation = __esm({
10980
+ "src/license/activation.ts"() {
10981
+ "use strict";
10982
+ init_banner();
10983
+ init_theme();
10984
+ init_verify();
10985
+ init_trial_policy();
10986
+ init_upgrade();
10987
+ }
10988
+ });
10989
+
10990
+ // src/demo/scenarios.ts
10991
+ var scenarios_exports = {};
10992
+ __export(scenarios_exports, {
10993
+ NAMED_DEMO_SCENARIOS: () => NAMED_DEMO_SCENARIOS,
10994
+ SCENARIOS: () => SCENARIOS,
10995
+ SCENARIO_LIST: () => SCENARIO_LIST,
10996
+ blendScenarios: () => blendScenarios,
10997
+ getScenario: () => getScenario,
10998
+ isNamedDemoScenario: () => isNamedDemoScenario,
10999
+ pickRandomScenario: () => pickRandomScenario,
11000
+ resolveScenarioInput: () => resolveScenarioInput
11001
+ });
11002
+ function getScenario(key) {
11003
+ const scenario = SCENARIOS[key];
11004
+ if (!scenario) {
11005
+ throw new Error(`Unknown scenario: ${key}. Valid: ${Object.keys(SCENARIOS).join(", ")}`);
11006
+ }
11007
+ return scenario;
11008
+ }
11009
+ function isNamedDemoScenario(raw) {
11010
+ return NAMED_DEMO_SCENARIOS.includes(raw);
11011
+ }
11012
+ function resolveScenarioInput(raw) {
11013
+ const input = raw?.trim();
11014
+ if (!input) return void 0;
11015
+ if (input === "research_blend") return "research_blend";
11016
+ if (NAMED_DEMO_SCENARIOS.includes(input)) {
11017
+ return input;
11018
+ }
11019
+ const n = Number(input);
11020
+ if (Number.isInteger(n) && n >= 1 && n <= NAMED_DEMO_SCENARIOS.length) {
11021
+ return NAMED_DEMO_SCENARIOS[n - 1];
11022
+ }
11023
+ return null;
11024
+ }
11025
+ function pickRandomScenario() {
11026
+ return RANDOM_POOL[Math.floor(Math.random() * RANDOM_POOL.length)];
11027
+ }
11028
+ function blendScenarios(_research) {
11029
+ const blended = {
11030
+ ...BASELINE,
11031
+ key: "research_blend",
11032
+ label: "Research-Derived Blend",
11033
+ description: "Realistic data with mild-to-moderate problems across all vital signs.",
11034
+ story: "Generated with default research blend. Problems are seeded across all vital signs at realistic levels.",
11035
+ hook: "Mild-to-moderate problems seeded across all five vitals.",
11036
+ // Bump all problems slightly above baseline for discoverability
11037
+ staleContactRatio: 0.2,
11038
+ pastCloseDateRatio: 0.15,
11039
+ mqlDropRatio: 0.15,
11040
+ qualifiedNoOutreachRatio: 0.15,
11041
+ stuckDealRatio: 0.15,
11042
+ noiseActivityRatio: 0.2,
11043
+ singleThreadRatio: 0.25
11044
+ };
11045
+ return blended;
11046
+ }
11047
+ var BASELINE, SCENARIOS, SCENARIO_LIST, NAMED_DEMO_SCENARIOS, RANDOM_POOL;
11048
+ var init_scenarios = __esm({
11049
+ "src/demo/scenarios.ts"() {
11050
+ "use strict";
11051
+ BASELINE = {
11052
+ enterpriseRatio: 0.2,
11053
+ midMarketRatio: 0.3,
11054
+ smbRatio: 0.5,
11055
+ staleContactRatio: 0.15,
11056
+ staleContactRatioEnterprise: 0.2,
11057
+ staleContactRatioSmb: 0.1,
11058
+ pastCloseDateRatio: 0.1,
11059
+ staleDays: 120,
11060
+ mqlDropRatio: 0.1,
11061
+ qualifiedNoOutreachRatio: 0.1,
11062
+ stuckDealRatio: 0.1,
11063
+ stuckInNegotiationDays: 45,
11064
+ avgDaysPerStageEnterprise: 25,
11065
+ avgDaysPerStageSmb: 8,
11066
+ activityVolumeMultiplier: 1,
11067
+ noiseActivityRatio: 0.15,
11068
+ singleThreadRatio: 0.2,
11069
+ loneWolfRepIndex: null,
11070
+ loneWolfSingleThreadRatio: 0,
11071
+ freshnessGapDays: 90
11072
+ };
11073
+ SCENARIOS = {
11074
+ hidden_crisis: {
11075
+ ...BASELINE,
11076
+ key: "hidden_crisis",
11077
+ label: "The Hidden Crisis",
11078
+ description: "Overall health looks yellow but Enterprise is deep red, masked by strong SMB numbers.",
11079
+ story: "Your aggregate numbers look okay \u2014 but when you break it by segment, Enterprise is dying. 60% of enterprise contacts have gone dark, deals are single-threaded, and SMB is carrying the average.",
11080
+ hook: "SMB is carrying the average while Enterprise dies quietly.",
11081
+ staleContactRatio: 0.3,
11082
+ staleContactRatioEnterprise: 0.6,
11083
+ staleContactRatioSmb: 0.1,
11084
+ singleThreadRatio: 0.5,
11085
+ enterpriseRatio: 0.3,
11086
+ midMarketRatio: 0.3,
11087
+ smbRatio: 0.4
11088
+ },
11089
+ leaky_bucket: {
11090
+ ...BASELINE,
11091
+ key: "leaky_bucket",
11092
+ label: "The Leaky Bucket",
11093
+ description: "Marketing generates plenty of leads but 40% vanish at handoff to sales.",
11094
+ story: "Marketing is doing its job \u2014 MQLs are flowing. But 40% of qualified leads never show up in sales workflows. They're falling through the cracks at handoff, and nobody's noticing because marketing reports MQL count and sales reports pipeline value.",
11095
+ hook: "MQLs flow in, then 40% vanish at the sales handoff.",
11096
+ mqlDropRatio: 0.4,
11097
+ qualifiedNoOutreachRatio: 0.35,
11098
+ staleContactRatio: 0.2
11099
+ },
11100
+ stale_pipeline: {
11101
+ ...BASELINE,
11102
+ key: "stale_pipeline",
11103
+ label: "The Stale Pipeline",
11104
+ description: "Big pipeline number but half the deals are zombies stuck in late stages.",
11105
+ story: "The pipeline report says $5M. But look closer: half those deals have close dates in the past, 40% are stuck in Negotiation for 120+ days, and nobody's touching them. You're forecasting on fiction.",
11106
+ hook: "Half the pipeline is zombies \u2014 you're forecasting on fiction.",
11107
+ pastCloseDateRatio: 0.5,
11108
+ stuckDealRatio: 0.4,
11109
+ stuckInNegotiationDays: 120,
11110
+ staleContactRatio: 0.25,
11111
+ staleDays: 90
11112
+ },
11113
+ lone_wolf: {
11114
+ ...BASELINE,
11115
+ key: "lone_wolf",
11116
+ label: "The Lone Wolf",
11117
+ description: "One rep has great numbers but every single deal is single-threaded.",
11118
+ story: "Your top rep is crushing it on paper \u2014 biggest pipeline, highest close rate. But every deal has exactly one contact. One champion goes on vacation, gets promoted, or leaves, and the entire pipeline collapses.",
11119
+ hook: "Top rep, huge pipeline, one contact per deal \u2014 one exit from collapse.",
11120
+ loneWolfRepIndex: 0,
11121
+ loneWolfSingleThreadRatio: 1,
11122
+ singleThreadRatio: 0.15
11123
+ },
11124
+ busy_bees: {
11125
+ ...BASELINE,
11126
+ key: "busy_bees",
11127
+ label: "The Busy Bees",
11128
+ description: "High activity volume across the team, but most of it hits dead ends.",
11129
+ story: "Your team is busy. Activity metrics look great \u2014 calls are up, emails are up, meetings are up. But 60% of that activity is aimed at contacts with no associated pipeline. Reps are spraying, not aiming.",
11130
+ hook: "Reps are spraying, not aiming.",
11131
+ activityVolumeMultiplier: 3,
11132
+ noiseActivityRatio: 0.6,
11133
+ staleContactRatio: 0.2
11134
+ },
11135
+ even_keel: {
11136
+ ...BASELINE,
11137
+ key: "even_keel",
11138
+ label: "The Even Keel",
11139
+ description: "A reasonably healthy book \u2014 enough yellow to listen, not a five-alarm fire.",
11140
+ story: "Most numbers sit in a normal band. A few contacts have gone quiet, a handful of deals are slow, activity is mostly on-pipeline. This is what 'fine' looks like on the stethoscope \u2014 useful when you want to evaluate NTRP without a manufactured crisis.",
11141
+ hook: "Reasonably healthy \u2014 enough signal to listen, not a crisis."
11142
+ },
11143
+ compound_pain: {
11144
+ ...BASELINE,
11145
+ key: "compound_pain",
11146
+ label: "The Compound Fracture",
11147
+ description: "Several vitals are red at once \u2014 stale pipeline, leaky handoff, noisy activity, thin threads.",
11148
+ story: "This isn't one problem. Enterprise contacts have gone dark, MQLs vanish at handoff, late-stage deals are zombies, and a lot of activity never touches pipeline. The gating logic has to pick a first red \u2014 that's the point of this book.",
11149
+ hook: "Several vitals red at once \u2014 the stethoscope has to pick a first listen.",
11150
+ enterpriseRatio: 0.3,
11151
+ midMarketRatio: 0.3,
11152
+ smbRatio: 0.4,
11153
+ staleContactRatio: 0.35,
11154
+ staleContactRatioEnterprise: 0.55,
11155
+ staleContactRatioSmb: 0.15,
11156
+ pastCloseDateRatio: 0.35,
11157
+ staleDays: 100,
11158
+ mqlDropRatio: 0.3,
11159
+ qualifiedNoOutreachRatio: 0.25,
11160
+ stuckDealRatio: 0.3,
11161
+ stuckInNegotiationDays: 90,
11162
+ activityVolumeMultiplier: 2,
11163
+ noiseActivityRatio: 0.4,
11164
+ singleThreadRatio: 0.4
11165
+ }
11166
+ };
11167
+ SCENARIO_LIST = Object.values(SCENARIOS);
11168
+ NAMED_DEMO_SCENARIOS = [
11169
+ "hidden_crisis",
11170
+ "leaky_bucket",
11171
+ "stale_pipeline",
11172
+ "lone_wolf",
11173
+ "busy_bees",
11174
+ "even_keel",
11175
+ "compound_pain"
11176
+ ];
11177
+ RANDOM_POOL = [...NAMED_DEMO_SCENARIOS];
11178
+ }
11179
+ });
11180
+
11181
+ // src/baselines/metrics-benchmarks.ts
11182
+ function resolveMetricBenchmarks(motion) {
11183
+ return METRICS_BENCHMARKS[motion ?? "mid_market"];
11184
+ }
11185
+ function motionBenchmarkLabel(motion) {
11186
+ return MOTION_LABELS[motion ?? "mid_market"];
11187
+ }
11188
+ function metricStatusHigherIsBetter(value, threshold) {
11189
+ if (value >= threshold.green) return "green";
11190
+ if (value >= threshold.yellow) return "yellow";
11191
+ return "red";
11192
+ }
11193
+ var METRICS_BENCHMARKS, MOTION_LABELS;
11194
+ var init_metrics_benchmarks = __esm({
11195
+ "src/baselines/metrics-benchmarks.ts"() {
11196
+ "use strict";
11197
+ METRICS_BENCHMARKS = {
11198
+ plg: {
11199
+ nrr: { green: 110, yellow: 100 },
11200
+ grr: { green: 85, yellow: 75 },
11201
+ win_rate: { green: 25, yellow: 15 },
11202
+ pipeline_coverage: { green: 4, yellow: 2.5 },
11203
+ magic_number: { green: 1, yellow: 0.75 },
11204
+ payback_months: { green: 12, yellow: 18 }
11205
+ },
11206
+ smb_velocity: {
11207
+ nrr: { green: 105, yellow: 95 },
11208
+ grr: { green: 88, yellow: 78 },
11209
+ win_rate: { green: 22, yellow: 12 },
11210
+ pipeline_coverage: { green: 3.5, yellow: 2 },
11211
+ magic_number: { green: 0.9, yellow: 0.6 },
11212
+ payback_months: { green: 14, yellow: 20 }
11213
+ },
11214
+ mid_market: {
11215
+ nrr: { green: 100, yellow: 90 },
11216
+ grr: { green: 90, yellow: 80 },
11217
+ win_rate: { green: 20, yellow: 12 },
11218
+ pipeline_coverage: { green: 3, yellow: 2 },
11219
+ magic_number: { green: 0.75, yellow: 0.5 },
11220
+ payback_months: { green: 16, yellow: 22 }
11221
+ },
11222
+ enterprise: {
11223
+ nrr: { green: 95, yellow: 85 },
11224
+ grr: { green: 92, yellow: 82 },
11225
+ win_rate: { green: 15, yellow: 8 },
11226
+ pipeline_coverage: { green: 2.5, yellow: 1.5 },
11227
+ magic_number: { green: 0.6, yellow: 0.4 },
11228
+ payback_months: { green: 18, yellow: 24 }
11229
+ }
11230
+ };
11231
+ MOTION_LABELS = {
11232
+ plg: "PLG",
11233
+ smb_velocity: "SMB Velocity",
11234
+ mid_market: "Mid-Market",
11235
+ enterprise: "Enterprise"
11236
+ };
11237
+ }
11238
+ });
11239
+
11240
+ // src/data/metric-definitions.ts
11241
+ function pctBand(metric, motion) {
11242
+ const m = motion ?? "mid_market";
11243
+ const t = METRICS_BENCHMARKS[m][metric];
11244
+ return `${motionBenchmarkLabel(m)} green \u2265${t.green}${metric === "pipeline_coverage" ? "x" : "%"}, yellow \u2265${t.yellow}${metric === "pipeline_coverage" ? "x" : "%"}`;
11245
+ }
11246
+ function monthsBand(motion) {
11247
+ const m = motion ?? "mid_market";
11248
+ const t = METRICS_BENCHMARKS[m].payback_months;
11249
+ return `${motionBenchmarkLabel(m)} green \u2264${t.green}mo, yellow \u2264${t.yellow}mo`;
11250
+ }
11251
+ function magicBand(motion) {
11252
+ const m = motion ?? "mid_market";
11253
+ const t = METRICS_BENCHMARKS[m].magic_number;
11254
+ return `${motionBenchmarkLabel(m)} green \u2265${t.green}, yellow \u2265${t.yellow}`;
11255
+ }
11256
+ function getMetricExplainer(id) {
11257
+ return BY_ID.get(id);
11258
+ }
11259
+ function resolveMetricId(query) {
11260
+ const q = query.trim().toLowerCase().replace(/\s+/g, " ");
11261
+ if (!q) return void 0;
11262
+ if (BY_ID.has(q)) return q;
11263
+ const direct = ALIAS_INDEX.get(q);
11264
+ if (direct) return direct;
11265
+ const norm = q.replace(/[-\s]+/g, "_");
11266
+ if (BY_ID.has(norm)) return norm;
11267
+ return ALIAS_INDEX.get(norm);
11268
+ }
11269
+ function listMetricExplainers(kind) {
11270
+ if (!kind) return METRIC_DEFINITIONS.slice();
11271
+ return METRIC_DEFINITIONS.filter((m) => m.kind === kind);
11272
+ }
11273
+ function getCoreDeckExplainers() {
11274
+ return CORE_DECK_IDS.map((id) => BY_ID.get(id)).filter(Boolean);
11275
+ }
11276
+ var VITALS, SAAS, METRIC_DEFINITIONS, CORE_DECK_IDS, BY_ID, ALIAS_INDEX, SAAS_METRIC_IDS, GATING_LAYER_VISUAL;
11277
+ var init_metric_definitions = __esm({
11278
+ "src/data/metric-definitions.ts"() {
11279
+ "use strict";
11280
+ init_metrics_benchmarks();
11281
+ VITALS = [
11282
+ {
11283
+ id: "freshness",
11284
+ kind: "vital",
11285
+ label: "Freshness",
11286
+ group: "Vital Signs",
11287
+ tagline: "Does the CRM report which records are still active?",
11288
+ how_computed: "NTRP computes a weighted average of people, organizations, and opportunities with recent activity. Open opportunities must also not be past-due. Default windows: people and organizations 90 days, opportunities 30 days. Weights are 35 / 30 / 35.",
11289
+ formula_lines: [
11290
+ "freshness = people%\xD70.35 + orgs%\xD70.30 + opps%\xD70.35",
11291
+ "people/orgs fresh if activity within 90d",
11292
+ "opps fresh if activity within 30d AND not past-due"
11293
+ ],
11294
+ meaning: "Board question: how much of this pipeline is real versus fiction? Dollar value equals the sum of amount on stale opportunities: pipeline at risk.",
11295
+ expert_read: "Cut by owner and by stage first. Freshness reds concentrate on people or process. They rarely spread evenly. In a long-cycle enterprise motion, 30 quiet days can be normal cadence. In a velocity motion, 30 quiet days is a dead deal. A sudden cliff usually means a broken integration or a departed rep. It is not gradual decay. Check this false positive: bulk-imported records that nobody has touched yet.",
11296
+ deepdive: [
11297
+ "Status bands: green 80 or more, yellow 60 or more, red below 60. Motion presets can change the windows.",
11298
+ 'Dollar translation: sum of amount on stale open opportunities \u2192 "pipeline at risk".',
11299
+ "Layer 1 of the gating stack. A red score here limits trust in later layers.",
11300
+ "Trigger play: Clean Dead Pipeline (clean-dead-pipeline) when the score is below 60.",
11301
+ "Levers: stale-deal alert at N quiet days. Weekly hygiene scrub. Enrichment refresh on quiet records. Signal-triggered reactivation for paid dormant accounts."
11302
+ ],
11303
+ visual: {
11304
+ kind: "bars",
11305
+ caption: "Example component mix. Higher bars are fresher.",
11306
+ bars: [
11307
+ { label: "People", value: 72, tone: "yellow" },
11308
+ { label: "Organizations", value: 81, tone: "green" },
11309
+ { label: "Opportunities", value: 44, tone: "red" }
11310
+ ]
11311
+ },
11312
+ play_id: "clean-dead-pipeline",
11313
+ dollar_label: "pipeline at risk",
11314
+ audience: {
11315
+ board: "Freshness answers whether the pipeline number is real. Low freshness means forecast risk. Stale deals inflate coverage and hide the true gap.",
11316
+ ops: "Score equals weighted recency across people, organizations, and opportunities. Cut by owner and stage. Set a stale-deal alert and a weekly scrub. Play: Clean Dead Pipeline."
11317
+ },
11318
+ aliases: ["data freshness", "stale", "zombie deals", "crm freshness"]
11319
+ },
11320
+ {
11321
+ id: "flow_rate",
11322
+ kind: "vital",
11323
+ label: "Flow Rate",
11324
+ group: "Vital Signs",
11325
+ tagline: "How fast do deals move, and where do they stop?",
11326
+ how_computed: "NTRP sets a base score from average open-deal age versus max_days. It then applies a penalty of up to 20 for the share of stuck deals. A deal is stuck when it has no update beyond stuck_days, or a past-due close. Status uses average open age, not the score alone.",
11327
+ formula_lines: [
11328
+ "base = 100 \xD7 (1 \u2212 avgOpenAge / max_days)",
11329
+ "score = base \u2212 stuckSharePenalty (\u226420)",
11330
+ "stuck = no update > stuck_days OR past-due close"
11331
+ ],
11015
11332
  meaning: "Board question: is next quarter slipping because deals are stuck? Dollar value equals the amount stuck in pipeline.",
11016
11333
  expert_read: "Cut by stage age, not only deal age. Find the stage where deals stop. That is usually one stage. Compare stuck-deal age to this company's own median cycle. Do not use a generic norm. Stuck deals plus past-due close dates signal optimistic forecasting. That is a credibility problem before it is a revenue problem.",
11017
11334
  deepdive: [
@@ -11580,714 +11897,399 @@ var init_metric_definitions = __esm({
11580
11897
  caption: "Example cycle versus motion norm",
11581
11898
  bars: [
11582
11899
  { label: "Your cycle", value: 78, tone: "yellow" },
11583
- { label: "Motion norm", value: 55, tone: "green" }
11584
- ]
11585
- },
11586
- audience: {
11587
- board: "Cycle stretch is an early soft signal that quality or process is slipping. This appears before the miss shows in bookings.",
11588
- ops: "Mean create to close on dated wins. Investigate the stage that aged."
11589
- },
11590
- aliases: ["sales cycle", "cycle length", "time to close"]
11591
- },
11592
- {
11593
- id: "stage_conversion",
11594
- kind: "saas",
11595
- label: "Stage Conversion",
11596
- group: "Sales Efficiency",
11597
- tagline: "Where in the stage model does advancement collapse?",
11598
- how_computed: "From metadata.stage_history stage advances when present. Otherwise a win-rate proxy.",
11599
- formula_lines: [
11600
- "Preferred: advancement rates from stage_history",
11601
- "Fallback: win-rate proxy when history is missing"
11602
- ],
11603
- meaning: "Board question: which single stage is starving everything downstream?",
11604
- expert_read: "Find the one stage where conversion collapses. That is the process problem. Everything downstream is starvation.",
11605
- deepdive: [
11606
- "Best with stage_history metadata. Otherwise treat this as a proxy.",
11607
- "Pairs with Flow Rate stage-age cuts."
11608
- ],
11609
- visual: {
11610
- kind: "funnel",
11611
- caption: "Example: find the collapse",
11612
- funnel: [
11613
- { label: "Stage 1\u21922", widthPct: 100 },
11614
- { label: "Stage 2\u21923", widthPct: 72 },
11615
- { label: "Stage 3\u21924", widthPct: 28 },
11616
- { label: "Stage 4\u2192Close", widthPct: 18 }
11617
- ]
11618
- },
11619
- audience: {
11620
- board: "Stage Conversion names the bottleneck stage. One collapse starves every stage after it.",
11621
- ops: "Prefer stage_history advances. Fix the collapse stage before you coach downstream reps."
11622
- },
11623
- aliases: ["stage conversion", "stage advance", "conversion by stage"]
11624
- },
11625
- // —— Unit economics ——
11626
- {
11627
- id: "ltv_proxy",
11628
- kind: "saas",
11629
- label: "LTV (Proxy)",
11630
- group: "Unit Economics",
11631
- tagline: "Rough lifetime value from deal size and GRR.",
11632
- how_computed: "avgDeal / ((100 \u2212 GRR) / 100) when GRR is below 100. This metric is unavailable when GRR is 100% or more, or missing.",
11633
- formula_lines: [
11634
- "LTV \u2248 avgDeal / churnRate",
11635
- "churnRate = (100 \u2212 GRR) / 100 (requires GRR < 100)"
11636
- ],
11637
- meaning: "Board question: what is a customer roughly worth over their life?",
11638
- expert_read: "This is a proxy, not a cohort LTV. Use it for direction, not capital allocation.",
11639
- deepdive: [
11640
- "Unavailable when GRR is 100 or more, or missing.",
11641
- "Pairs with CAC for LTV:CAC when spend data exists."
11642
- ],
11643
- visual: {
11644
- kind: "gauge",
11645
- caption: "Example LTV proxy. Directional only.",
11646
- gauge: 68
11647
- },
11648
- audience: {
11649
- board: "LTV Proxy is directional from deal size and GRR. It is not a cohort LTV. Use it for orientation, not capital decisions.",
11650
- ops: "avgDeal / ((100\u2212GRR)/100). Needs GRR below 100. Prefer cohort math when billing data arrives."
11651
- },
11652
- aliases: ["ltv", "lifetime value"]
11653
- },
11654
- {
11655
- id: "cac",
11656
- kind: "saas",
11657
- label: "CAC",
11658
- group: "Unit Economics",
11659
- tagline: "Customer acquisition cost. This metric needs spend data.",
11660
- how_computed: "This metric requires campaign or sales spend data. It is currently unavailable on CRM-only datasets.",
11661
- formula_lines: ["CAC = sales & marketing spend / new customers", "(requires spend data \u2014 not in CRM-only exports)"],
11662
- meaning: "Board question: what does a new logo cost to win?",
11663
- expert_read: "Without spend, NTRP cannot invent CAC. Connect campaign spend or finance exports to unlock unit economics.",
11664
- deepdive: [
11665
- "Always unavailable on CRM-only demos. That is expected.",
11666
- "Unlocks LTV:CAC, Payback, and Magic Number when spend lands."
11667
- ],
11668
- visual: { kind: "none", caption: "Needs campaign spend or a finance export" },
11669
- audience: {
11670
- board: "CAC is locked until spend data is connected. CRM alone cannot price acquisition.",
11671
- ops: "Bring campaign or S&M spend. Until then, unit-econ metrics stay unavailable by design."
11672
- },
11673
- aliases: ["customer acquisition cost", "acquisition cost"]
11674
- },
11675
- {
11676
- id: "ltv_cac_ratio",
11677
- kind: "saas",
11678
- label: "LTV:CAC Ratio",
11679
- group: "Unit Economics",
11680
- tagline: "Does acquisition spend return enough value?",
11681
- how_computed: "LTV proxy divided by CAC. Unavailable without spend (CAC).",
11682
- formula_lines: ["LTV:CAC = LTV_proxy / CAC", "(requires CAC)"],
11683
- meaning: "Board question: do we earn enough lifetime value per dollar spent to acquire?",
11684
- expert_read: "In the efficiency era, boards weigh LTV:CAC and payback as heavily as growth. Classic rule of thumb is 3x or more. Motion and gross margin matter.",
11685
- deepdive: ["Blocked on CAC. See LTV Proxy and CAC."],
11686
- visual: { kind: "none", caption: "This metric needs CAC (spend data)" },
11687
- audience: {
11688
- board: "LTV:CAC is the acquisition ROI story. It is available once spend is connected.",
11689
- ops: "LTV_proxy / CAC. Unlocks with spend import."
11690
- },
11691
- aliases: ["ltv cac", "ltv/cac", "ltv to cac"]
11692
- },
11693
- {
11694
- id: "payback_months",
11695
- kind: "saas",
11696
- label: "Payback Months",
11697
- group: "Unit Economics",
11698
- tagline: "How many months to recover CAC?",
11699
- how_computed: "This metric requires CAC and spend. Lower is better.",
11700
- formula_lines: ["Payback \u2248 CAC / (monthly gross profit per customer)", "(requires spend data)"],
11701
- meaning: "Board question: how fast does acquisition spend return? Efficiency-era prior: under 18 months is often healthy.",
11702
- expert_read: "Boards now weigh payback (under 18 months) as heavily as growth in many motions.",
11703
- deepdive: ["Blocked on CAC. Benchmarks exist per motion once data lands."],
11704
- visual: { kind: "none", caption: "This metric needs CAC (spend data)" },
11900
+ { label: "Motion norm", value: 55, tone: "green" }
11901
+ ]
11902
+ },
11705
11903
  audience: {
11706
- board: "Payback is how fast CAC returns. Efficiency-era boards often want under 18 months.",
11707
- ops: "Requires CAC. Motion green and yellow bands apply when available."
11904
+ board: "Cycle stretch is an early soft signal that quality or process is slipping. This appears before the miss shows in bookings.",
11905
+ ops: "Mean create to close on dated wins. Investigate the stage that aged."
11708
11906
  },
11709
- aliases: ["payback", "cac payback"],
11710
- benchmarkHint: (motion) => monthsBand(motion)
11907
+ aliases: ["sales cycle", "cycle length", "time to close"]
11711
11908
  },
11712
11909
  {
11713
- id: "magic_number",
11910
+ id: "stage_conversion",
11714
11911
  kind: "saas",
11715
- label: "Magic Number",
11716
- group: "Unit Economics",
11717
- tagline: "Sales efficiency: net new ARR per sales dollar.",
11718
- how_computed: "This metric requires sales spend. Classic form: net new ARR (quarter) / prior-quarter S&M spend.",
11912
+ label: "Stage Conversion",
11913
+ group: "Sales Efficiency",
11914
+ tagline: "Where in the stage model does advancement collapse?",
11915
+ how_computed: "From metadata.stage_history stage advances when present. Otherwise a win-rate proxy.",
11719
11916
  formula_lines: [
11720
- "Magic Number \u2248 Net New ARR(q) / S&M spend(q\u22121)",
11721
- "(requires spend data)"
11917
+ "Preferred: advancement rates from stage_history",
11918
+ "Fallback: win-rate proxy when history is missing"
11722
11919
  ],
11723
- meaning: "Board question: how efficiently does sales spend produce net new ARR? Prior: above 0.75 is often healthy. Above 1 is strong.",
11724
- expert_read: "In the efficiency era, a magic number above 0.75 is weighed alongside growth. Without spend, NTRP keeps this unavailable. NTRP does not invent it.",
11725
- deepdive: ["Blocked on spend. Benchmarks per motion are ready when data lands."],
11726
- visual: { kind: "none", caption: "This metric needs S&M spend data" },
11727
- audience: {
11728
- board: "Magic Number prices sales efficiency. It is available once S&M spend is connected.",
11729
- ops: "Net new ARR / prior S&M. Motion benchmarks apply when spend lands."
11730
- },
11731
- aliases: ["sales magic number", "sales efficiency magic number"],
11732
- benchmarkHint: (motion) => magicBand(motion)
11733
- }
11734
- ];
11735
- METRIC_DEFINITIONS = [...VITALS, ...SAAS];
11736
- CORE_DECK_IDS = [
11737
- "arr",
11738
- "nrr",
11739
- "grr",
11740
- "pipeline_coverage",
11741
- "win_rate",
11742
- "pipeline_velocity",
11743
- "freshness",
11744
- "flow_rate",
11745
- "drop_rate",
11746
- "signal_to_noise",
11747
- "thread_depth"
11748
- ];
11749
- BY_ID = new Map(METRIC_DEFINITIONS.map((m) => [m.id, m]));
11750
- ALIAS_INDEX = (() => {
11751
- const idx = /* @__PURE__ */ new Map();
11752
- for (const m of METRIC_DEFINITIONS) {
11753
- idx.set(m.id.toLowerCase(), m.id);
11754
- idx.set(m.label.toLowerCase(), m.id);
11755
- for (const a of m.aliases ?? []) {
11756
- idx.set(a.toLowerCase(), m.id);
11757
- }
11758
- }
11759
- idx.set("signal-to-noise", "signal_to_noise");
11760
- idx.set("signal:noise", "signal_to_noise");
11761
- idx.set("flow-rate", "flow_rate");
11762
- idx.set("drop-rate", "drop_rate");
11763
- idx.set("thread-depth", "thread_depth");
11764
- return idx;
11765
- })();
11766
- SAAS_METRIC_IDS = SAAS.map((m) => m.id);
11767
- GATING_LAYER_VISUAL = {
11768
- kind: "layer_stack",
11769
- caption: "Gating order. First red in layer order wins. If none, first yellow. If none, lowest score.",
11770
- layers: [
11771
- { label: "L1 Freshness", highlight: true },
11772
- { label: "L2 Flow Rate \xB7 Drop Rate" },
11773
- { label: "L3 Signal:Noise" },
11774
- { label: "L4 Thread Depth" }
11775
- ]
11776
- };
11777
- }
11778
- });
11779
-
11780
- // src/data/guide-slides.ts
11781
- function normalize(raw) {
11782
- return raw.trim().toLowerCase().replace(/\s+/g, "-").replace(/_/g, "-");
11783
- }
11784
- function isGuideSectionQuery(raw) {
11785
- const q = normalize(raw);
11786
- return GUIDE_SECTION_ALIASES.includes(q);
11787
- }
11788
- function resolveGuideId(raw) {
11789
- const q = normalize(raw);
11790
- if (!q) return null;
11791
- if (isGuideSectionQuery(q)) return GUIDE_DECK_IDS[0];
11792
- const underscored = q.replace(/-/g, "_");
11793
- if (GUIDE_DECK_IDS.includes(underscored)) {
11794
- return underscored;
11795
- }
11796
- return SLIDE_ALIASES[q] ?? SLIDE_ALIASES[underscored] ?? null;
11797
- }
11798
- function getGuideSlide(id) {
11799
- return BY_ID2.get(id);
11800
- }
11801
- function listGuideSlides() {
11802
- return GUIDE_DECK_IDS.map((id) => BY_ID2.get(id));
11803
- }
11804
- var GUIDE_DECK_IDS, GUIDE_SECTION_ALIASES, SLIDE_ALIASES, TALK, ASK, HANDOFF, LOOP, BY_ID2;
11805
- var init_guide_slides = __esm({
11806
- "src/data/guide-slides.ts"() {
11807
- "use strict";
11808
- GUIDE_DECK_IDS = ["talk", "ask", "handoff", "loop"];
11809
- GUIDE_SECTION_ALIASES = [
11810
- "guide",
11811
- "how",
11812
- "howto",
11813
- "how-to",
11814
- "how_to",
11815
- "use",
11816
- "using",
11817
- "nav",
11818
- "navigate",
11819
- "userflow",
11820
- "user-guide",
11821
- "userguide"
11822
- ];
11823
- SLIDE_ALIASES = {
11824
- talk: "talk",
11825
- chat: "talk",
11826
- type: "talk",
11827
- speak: "talk",
11828
- conversation: "talk",
11829
- ask: "ask",
11830
- connect: "ask",
11831
- glossary: "ask",
11832
- keyless: "ask",
11833
- enter: "ask",
11834
- definitions: "ask",
11835
- handoff: "handoff",
11836
- inbox: "handoff",
11837
- skill: "handoff",
11838
- claude: "handoff",
11839
- ship: "handoff",
11840
- pickup: "handoff",
11841
- chatgpt: "handoff",
11842
- cursor: "handoff",
11843
- export: "handoff",
11844
- loop: "loop",
11845
- strategy: "loop",
11846
- strategist: "loop",
11847
- playbook: "loop",
11848
- remember: "loop",
11849
- sessions: "loop",
11850
- progress: "loop"
11851
- };
11852
- TALK = {
11853
- id: "talk",
11854
- label: "Talk to NTRP",
11855
- tagline: "Type English. Confirm the scope. Load data. Then listen.",
11856
- visual: {
11857
- kind: "funnel",
11858
- caption: "Bare Enter submits the dim \u23CE hint at these gates. If nothing is armed, Enter does nothing.",
11859
- funnel: [
11860
- { label: "Type a question", widthPct: 100 },
11861
- { label: "\u23CE yes (scope)", widthPct: 78 },
11862
- { label: "Load data", widthPct: 56 },
11863
- { label: "\u23CE go ahead", widthPct: 34 }
11864
- ]
11865
- },
11866
- lines: [
11867
- 'You do not need a slash. Type the question you need. Examples: "is our retention real for the board?" or "pipeline health".',
11868
- "",
11869
- "NTRP restates the question as a scope card. Confirm with \u23CE yes, or type yes. NTRP listens. NTRP does not invent a different question.",
11870
- "",
11871
- "When the dataset is empty, type \u23CE use demo data. Or paste a CSV path. Or type /ingest. When the gap card shows that the formulas can compute, type \u23CE go ahead.",
11872
- "",
11873
- "Vital signs and SaaS metrics compute without an AI key. Every score that has a dollar translation shows it."
11874
- ],
11875
- deepdive: [
11876
- "Power-user slash commands still work. They stay hidden from /help: /new, /diagnose, /metrics, /ingest, /session.",
11877
- 'Type "use demo data" to load the hidden_crisis scenario. Company profile is optional. In scripts, type /demo --no-profile.',
11878
- 'After compute, the prompt becomes ask \u203A. Brief is the default depth. Type "go deep" when you want the long read.',
11879
- "Type /home to show phase status. Type /help to list conversation shortcuts. /help does not list every command."
11880
- ]
11881
- };
11882
- ASK = {
11883
- id: "ask",
11884
- label: "After the numbers",
11885
- tagline: 'Glossary is free. "Our ARR" is compute. Narrative needs /connect.',
11886
- visual: {
11887
- kind: "split",
11888
- caption: "Possessives such as our, my, and the team's skip the glossary. They go to compute or scope.",
11889
- bars: [
11890
- { label: "what is ARR?", value: 55, tone: "accent" },
11891
- { label: "our ARR", value: 45, tone: "neutral" }
11892
- ]
11893
- },
11894
- lines: [
11895
- '"what is ARR?" and "how is freshness calculated?" answer from the built-in glossary. No key. No data.',
11896
- "",
11897
- '"what is our ARR?" and "why is this red?" need a loaded dataset. AI findings and /ask need a stored key. Type /connect and paste any provider key. NTRP detects it.',
11898
- "",
11899
- "Type /deepdive to replay this tour. Type /deepdive freshness for one slide. You can also type nrr, arr, or another id. Type /deepdive list for the catalog. Type /deepdive guide for this how-to section.",
11900
- "",
11901
- "When the prompt shows a dim \u23CE hint, bare Enter submits that action. Examples: \u23CE yes, \u23CE use demo data, \u23CE go ahead, \u23CE /connect. If there is no hint, Enter does nothing."
11902
- ],
11903
- deepdive: [
11904
- "Type /connect ollama for a keyless local model. Type /connect --base-url <url> --id <name> for any OpenAI-compatible endpoint.",
11905
- "Type /model refresh to re-discover models. A retired model self-heals on the first 404.",
11906
- "First-run skip of this tour does not complete it. A home \u2691 chip can bring you back after the first analysis.",
11907
- "Tab completes /deepdive <metric>. Finding cards and playbook triggers also link here."
11908
- ]
11909
- };
11910
- HANDOFF = {
11911
- id: "handoff",
11912
- label: "Ship work to Claude",
11913
- tagline: "Teach the inbox once. Later, tell Claude to pick it up.",
11914
- visual: {
11915
- kind: "layer_stack",
11916
- caption: "You and Claude find and open the file. NTRP only writes.",
11917
- layers: [
11918
- { label: "Set a pickup folder (demo, onboard, or /inbox set)" },
11919
- { label: "Paste the finder skill once (/inbox skill)", highlight: true },
11920
- { label: "/handoff writes files. There is no paste block." },
11921
- { label: '"Pick up the latest NTRP handoff."' }
11922
- ]
11923
- },
11924
- lines: [
11925
- 'Type "ship a board deck" or type /handoff. Files land in ~/Documents/Claude/ntrp-inbox by default. Type /inbox set to change the folder.',
11926
- "",
11927
- "During demo or company setup, or any time, type /inbox skill. Paste a standing finder into Claude, ChatGPT, or Cursor once. That skill tells the tool to follow latest-handoff.md and INDEX.md.",
11928
- "",
11929
- 'After that, each /handoff prints "Handoff ready". You will not get a paste block every write. Tell Claude: pick up the latest NTRP handoff.',
11930
- "",
11931
- "If you skipped this step, NTRP asks once when you load your own data. Skip then, and type /inbox set ~/Documents/Claude/ntrp-inbox. Then type /inbox skill. Type /handoff skill to reprint the same finder."
11932
- ],
11933
- deepdive: [
11934
- "Inbox copies: latest-handoff.md, latest-pickup.md, SKILL.md, INDEX.md. Dated files also live under ~/.ntrp/exports/.",
11935
- "Type /exports to list writes. Type /inbox show to print the folder and the latest pointer. Type /handoff --print to show the prompt body in the terminal.",
11936
- "Audience-framed Metric definitions append to decks and reports. Claude then has the same glossary you walked.",
11937
- "Type /end to close a session. NTRP writes a transcript plus a 1-page context brief under ~/.ntrp/sessions/."
11938
- ]
11939
- };
11940
- LOOP = {
11941
- id: "loop",
11942
- label: "Stay in the loop",
11943
- tagline: "Diagnose \u2192 plan \u2192 review \u2192 remember. NTRP learns your business.",
11944
- visual: {
11945
- kind: "layer_stack",
11946
- caption: "Stethoscope, not hospital. Observe and recommend. Never prescribe surgery.",
11947
- layers: [
11948
- { label: "Listen (/diagnose, /metrics)" },
11949
- { label: 'Plan ("how should we fix this?" / /strategy)', highlight: true },
11950
- { label: "Review (/strategy review, /playbook)" },
11951
- { label: "Remember (/remember, /rate, ANALYST.md)" }
11952
- ]
11920
+ meaning: "Board question: which single stage is starving everything downstream?",
11921
+ expert_read: "Find the one stage where conversion collapses. That is the process problem. Everything downstream is starvation.",
11922
+ deepdive: [
11923
+ "Best with stage_history metadata. Otherwise treat this as a proxy.",
11924
+ "Pairs with Flow Rate stage-age cuts."
11925
+ ],
11926
+ visual: {
11927
+ kind: "funnel",
11928
+ caption: "Example: find the collapse",
11929
+ funnel: [
11930
+ { label: "Stage 1\u21922", widthPct: 100 },
11931
+ { label: "Stage 2\u21923", widthPct: 72 },
11932
+ { label: "Stage 3\u21924", widthPct: 28 },
11933
+ { label: "Stage 4\u2192Close", widthPct: 18 }
11934
+ ]
11935
+ },
11936
+ audience: {
11937
+ board: "Stage Conversion names the bottleneck stage. One collapse starves every stage after it.",
11938
+ ops: "Prefer stage_history advances. Fix the collapse stage before you coach downstream reps."
11939
+ },
11940
+ aliases: ["stage conversion", "stage advance", "conversion by stage"]
11953
11941
  },
11954
- lines: [
11955
- 'Type "how should we fix this?" or type /strategy. NTRP builds a measurable plan with milestones and dollar-anchored ranges. Type /strategy review to check those against later vital signs.',
11956
- "",
11957
- "When a vital sign is red, type /playbook. NTRP names the matching play. Outcomes from reviews annotate the catalog with what hit here.",
11958
- "",
11959
- "Type /remember to store a durable fact. Type /rate bad <reason> to write a calibration. Optional ~/.ntrp/ANALYST.md is standing voice and priorities. It never overrides safety rules.",
11960
- "",
11961
- "Commands: /sessions show, /session <id> pickup, /end (transcript plus brief), /home, /progress, /help."
11962
- ],
11963
- deepdive: [
11964
- 'Type "build me a game plan" before analysis. NTRP queues the strategist and resumes after compute.',
11965
- "Interactive sessions distill 5 or fewer durable facts on close when an LLM key is stored. One-shot commands do not bank hours or distill.",
11966
- "Type /scratch to factory-reset data and config. progress.json and install.json survive unless you pass --include-progress.",
11967
- "Credits in /progress accrue in interactive ntrp only."
11968
- ]
11969
- };
11970
- BY_ID2 = /* @__PURE__ */ new Map([
11971
- [TALK.id, TALK],
11972
- [ASK.id, ASK],
11973
- [HANDOFF.id, HANDOFF],
11974
- [LOOP.id, LOOP]
11975
- ]);
11976
- }
11977
- });
11978
-
11979
- // src/ui/slides.ts
11980
- import chalk9 from "chalk";
11981
- function liveFromVital(vs) {
11982
- const dollarLine = vs.dollar_value != null && vs.dollar_value > 0 ? `$${vs.dollar_value >= 1e6 ? `${(vs.dollar_value / 1e6).toFixed(1)}M` : vs.dollar_value >= 1e3 ? `${(vs.dollar_value / 1e3).toFixed(0)}K` : vs.dollar_value.toFixed(0)} ${vs.dollar_label ?? ""}`.trim() : void 0;
11983
- return {
11984
- formatted: String(Math.round(vs.score)),
11985
- status: vs.status,
11986
- dollarLine
11987
- };
11988
- }
11989
- function liveFromMetric(m) {
11990
- return {
11991
- formatted: m.formatted,
11992
- status: m.status === "neutral" ? "neutral" : m.status,
11993
- benchmarkNote: m.benchmark_note
11994
- };
11995
- }
11996
- function clearSlideScreen() {
11997
- if (process.stdout.isTTY) {
11998
- process.stdout.write("\x1B[2J\x1B[H");
11999
- }
12000
- }
12001
- function slideCardWidth() {
12002
- return resolveCardWidth({ min: 64, max: 100, margin: 4 });
12003
- }
12004
- function tonePaint(tone = "accent") {
12005
- switch (tone) {
12006
- case "green":
12007
- return chalk9.hex("#22c55e");
12008
- case "yellow":
12009
- return chalk9.hex("#eab308");
12010
- case "red":
12011
- return chalk9.hex("#ef4444");
12012
- case "neutral":
12013
- return chalk9.dim;
12014
- default:
12015
- return (t) => paint("accent", t);
12016
- }
12017
- }
12018
- function renderBarRow(bar, barWidth, labelW) {
12019
- const fill = Math.max(0, Math.min(barWidth, Math.round(bar.value / 100 * barWidth)));
12020
- const body = "\u2588".repeat(fill) + "\u2591".repeat(barWidth - fill);
12021
- const colored = tonePaint(bar.tone)(body);
12022
- const label = padRight(truncateVisible(bar.label, labelW), labelW);
12023
- const pct = String(Math.round(bar.value)).padStart(3);
12024
- return `${label} ${colored} ${chalk9.dim(pct)}`;
12025
- }
12026
- function renderBars(bars, inner) {
12027
- const labelW = Math.min(18, Math.max(...bars.map((b) => visibleWidth(b.label)), 8));
12028
- const barWidth = Math.max(8, Math.min(28, inner - labelW - 6));
12029
- return bars.map((b) => renderBarRow(b, barWidth, labelW));
12030
- }
12031
- function renderFunnel(steps, inner) {
12032
- const maxBar = Math.max(12, Math.min(40, inner - 22));
12033
- const lines = [];
12034
- for (const step of steps) {
12035
- const w = Math.max(2, Math.round(step.widthPct / 100 * maxBar));
12036
- const bar = paint("accent", "\u2588".repeat(w));
12037
- const label = truncateVisible(step.label, Math.max(8, inner - maxBar - 8));
12038
- lines.push(`${padRight(label, Math.min(18, inner - maxBar - 6))} ${bar} ${chalk9.dim(`${step.widthPct}%`)}`);
12039
- }
12040
- return lines;
12041
- }
12042
- function renderWaterfall(steps, inner) {
12043
- const maxAbs = Math.max(...steps.map((s) => Math.abs(s.cumulative)), 1);
12044
- const barW = Math.max(10, Math.min(28, inner - 28));
12045
- const lines = [];
12046
- for (const step of steps) {
12047
- const fill = Math.max(1, Math.round(Math.abs(step.cumulative) / maxAbs * barW));
12048
- const bar = step.delta >= 0 ? chalk9.hex("#22c55e")("\u2588".repeat(fill)) : chalk9.hex("#ef4444")("\u2588".repeat(fill));
12049
- const deltaStr = step.delta > 0 ? `+${step.delta}` : step.delta < 0 ? `${step.delta}` : `${step.delta}`;
12050
- const deltaPainted = step.delta > 0 ? chalk9.hex("#22c55e")(deltaStr.padStart(5)) : step.delta < 0 ? chalk9.hex("#ef4444")(deltaStr.padStart(5)) : chalk9.dim(deltaStr.padStart(5));
12051
- const label = padRight(truncateVisible(step.label, 14), 14);
12052
- lines.push(`${label} ${deltaPainted} ${bar} ${chalk9.dim(`\u2192 ${step.cumulative}`)}`);
12053
- }
12054
- return lines;
12055
- }
12056
- function renderLayerStack(layers, inner) {
12057
- const lines = [];
12058
- for (let i = 0; i < layers.length; i++) {
12059
- const layer = layers[i];
12060
- const marker2 = layer.highlight ? paint("accent", "\u25C6") : chalk9.dim("\u25C7");
12061
- const text = layer.highlight ? bold(layer.label) : chalk9.dim(layer.label);
12062
- lines.push(`${marker2} ${truncateVisible(text, inner - 4)}`);
12063
- if (i < layers.length - 1) {
12064
- lines.push(chalk9.dim(" \u2502"));
12065
- }
12066
- }
12067
- return lines;
12068
- }
12069
- function renderLevers(levers, inner) {
12070
- const cell = Math.floor((inner - 9) / 2);
12071
- const lines = [];
12072
- lines.push(chalk9.dim("\u250C" + "\u2500".repeat(Math.max(8, cell)) + "\u2510 \u250C" + "\u2500".repeat(Math.max(8, cell)) + "\u2510"));
12073
- for (let i = 0; i < levers.length; i += 2) {
12074
- const a = padRight(truncateVisible(levers[i] ?? "", cell - 2), cell - 2);
12075
- const b = padRight(truncateVisible(levers[i + 1] ?? "", cell - 2), cell - 2);
12076
- lines.push(
12077
- `${paint("accent", "\u2502")} ${a} ${paint("accent", "\u2502")} ${paint("accent", "\u2502")} ${b} ${paint("accent", "\u2502")}`
12078
- );
12079
- }
12080
- lines.push(chalk9.dim("\u2514" + "\u2500".repeat(Math.max(8, cell)) + "\u2518 \u2514" + "\u2500".repeat(Math.max(8, cell)) + "\u2518"));
12081
- return lines;
12082
- }
12083
- function renderGauge(score, inner, status) {
12084
- const st = status ?? (score >= 80 ? "green" : score >= 60 ? "yellow" : "red");
12085
- const bar = scoreBar(score, st === "neutral" ? "yellow" : st, Math.min(28, inner - 12));
12086
- return [`${statusDot(st)} ${bar} ${bold(String(Math.round(score)))}`];
12087
- }
12088
- function renderSplit(bars, inner) {
12089
- if (bars.length < 2) return renderBars(bars, inner);
12090
- const total = bars.reduce((s, b) => s + b.value, 0) || 100;
12091
- const width = Math.max(16, Math.min(40, inner - 4));
12092
- let used = 0;
12093
- const parts = [];
12094
- for (let i = 0; i < bars.length; i++) {
12095
- const b = bars[i];
12096
- const w = i === bars.length - 1 ? width - used : Math.max(1, Math.round(b.value / total * width));
12097
- used += w;
12098
- parts.push(tonePaint(b.tone)("\u2588".repeat(w)));
12099
- }
12100
- const legend = bars.map((b) => `${tonePaint(b.tone)("\u25CF")} ${b.label} ${chalk9.dim(`${Math.round(b.value)}%`)}`).join(" ");
12101
- return [parts.join(""), truncateVisible(legend, inner)];
12102
- }
12103
- function renderVisual(visual, inner) {
12104
- const lines = [];
12105
- switch (visual.kind) {
12106
- case "bars":
12107
- if (visual.bars?.length) lines.push(...renderBars(visual.bars, inner));
12108
- break;
12109
- case "funnel":
12110
- if (visual.funnel?.length) lines.push(...renderFunnel(visual.funnel, inner));
12111
- break;
12112
- case "waterfall":
12113
- if (visual.waterfall?.length) lines.push(...renderWaterfall(visual.waterfall, inner));
12114
- break;
12115
- case "layer_stack":
12116
- if (visual.layers?.length) lines.push(...renderLayerStack(visual.layers, inner));
12117
- break;
12118
- case "levers":
12119
- if (visual.levers?.length) lines.push(...renderLevers(visual.levers, inner));
12120
- break;
12121
- case "gauge":
12122
- lines.push(...renderGauge(visual.gauge ?? 50, inner));
12123
- if (visual.bars?.length) lines.push(...renderBars(visual.bars, inner));
12124
- break;
12125
- case "split":
12126
- if (visual.bars?.length) lines.push(...renderSplit(visual.bars, inner));
12127
- break;
12128
- case "none":
12129
- default:
12130
- break;
12131
- }
12132
- if (visual.caption) {
12133
- lines.push(chalk9.dim(truncateVisible(visual.caption, inner)));
12134
- }
12135
- return lines;
12136
- }
12137
- function kindBadge(explainer) {
12138
- if (explainer.kind === "vital") return badge("VITAL", "accent");
12139
- return badge("SAAS", "info");
12140
- }
12141
- function statusToTone(status) {
12142
- if (status === "green") return "success";
12143
- if (status === "yellow") return "warning";
12144
- if (status === "red") return "error";
12145
- return "muted";
12146
- }
12147
- function pushWrapped(out, text, inner, indent = "") {
12148
- for (const w of wrapWords(text, inner - indent.length)) {
12149
- out.push(indent + w);
12150
- }
12151
- }
12152
- function buildSlideContent(explainer, opts = {}) {
12153
- const width = slideCardWidth();
12154
- const inner = width - 4;
12155
- const lines = [];
12156
- const title = opts.titleOverride ?? (explainer ? explainer.label : "Metrics");
12157
- if (explainer) {
12158
- const headerBits = [
12159
- kindBadge(explainer),
12160
- chalk9.dim(explainer.group)
12161
- ];
12162
- if (opts.index != null && opts.total != null) {
12163
- headerBits.push(chalk9.dim(`slide ${opts.index}/${opts.total}`));
12164
- }
12165
- lines.push(headerBits.join(chalk9.dim(" \xB7 ")));
12166
- lines.push(chalk9.dim(explainer.tagline));
12167
- lines.push("");
12168
- if (opts.live) {
12169
- const live = opts.live;
12170
- const tone = statusToTone(live.status);
12171
- const liveLine = `${statusDot(live.status)} ${bold("Your reading:")} ${bold(live.formatted)} ` + badge(String(live.status), tone);
12172
- lines.push(truncateVisible(liveLine, inner));
12173
- if (live.dollarLine) {
12174
- lines.push(chalk9.dim(` $ ${live.dollarLine}`));
12175
- }
12176
- if (live.benchmarkNote) {
12177
- lines.push(chalk9.dim(` ${live.benchmarkNote}`));
12178
- }
12179
- lines.push("");
12180
- } else {
12181
- const hint = explainer.benchmarkHint?.(opts.motion);
12182
- if (hint) {
12183
- lines.push(chalk9.dim(`Benchmark \xB7 ${hint}`));
12184
- lines.push("");
12185
- }
12186
- }
12187
- const visual = opts.visualOverride ?? explainer.visual;
12188
- const visLines = renderVisual(visual, inner);
12189
- if (visLines.length) {
12190
- lines.push(...visLines);
12191
- lines.push("");
12192
- }
12193
- lines.push(sectionHeading("What it means"));
12194
- pushWrapped(lines, explainer.meaning, inner, " ");
12195
- lines.push("");
12196
- if (!opts.skipFormula) {
12197
- lines.push(sectionHeading("How it's calculated"));
12198
- pushWrapped(lines, explainer.how_computed, inner, " ");
12199
- for (const f of explainer.formula_lines) {
12200
- lines.push(paint("accent", ` ${f}`));
12201
- }
12202
- lines.push("");
12203
- }
12204
- if (opts.deepdive) {
12205
- lines.push(sectionHeading("Deep dive"));
12206
- pushWrapped(lines, explainer.expert_read, inner, " ");
12207
- lines.push("");
12208
- for (const bullet of explainer.deepdive) {
12209
- pushWrapped(lines, `\xB7 ${bullet}`, inner, " ");
11942
+ // —— Unit economics ——
11943
+ {
11944
+ id: "ltv_proxy",
11945
+ kind: "saas",
11946
+ label: "LTV (Proxy)",
11947
+ group: "Unit Economics",
11948
+ tagline: "Rough lifetime value from deal size and GRR.",
11949
+ how_computed: "avgDeal / ((100 \u2212 GRR) / 100) when GRR is below 100. This metric is unavailable when GRR is 100% or more, or missing.",
11950
+ formula_lines: [
11951
+ "LTV \u2248 avgDeal / churnRate",
11952
+ "churnRate = (100 \u2212 GRR) / 100 (requires GRR < 100)"
11953
+ ],
11954
+ meaning: "Board question: what is a customer roughly worth over their life?",
11955
+ expert_read: "This is a proxy, not a cohort LTV. Use it for direction, not capital allocation.",
11956
+ deepdive: [
11957
+ "Unavailable when GRR is 100 or more, or missing.",
11958
+ "Pairs with CAC for LTV:CAC when spend data exists."
11959
+ ],
11960
+ visual: {
11961
+ kind: "gauge",
11962
+ caption: "Example LTV proxy. Directional only.",
11963
+ gauge: 68
11964
+ },
11965
+ audience: {
11966
+ board: "LTV Proxy is directional from deal size and GRR. It is not a cohort LTV. Use it for orientation, not capital decisions.",
11967
+ ops: "avgDeal / ((100\u2212GRR)/100). Needs GRR below 100. Prefer cohort math when billing data arrives."
11968
+ },
11969
+ aliases: ["ltv", "lifetime value"]
11970
+ },
11971
+ {
11972
+ id: "cac",
11973
+ kind: "saas",
11974
+ label: "CAC",
11975
+ group: "Unit Economics",
11976
+ tagline: "Customer acquisition cost. This metric needs spend data.",
11977
+ how_computed: "This metric requires campaign or sales spend data. It is currently unavailable on CRM-only datasets.",
11978
+ formula_lines: ["CAC = sales & marketing spend / new customers", "(requires spend data \u2014 not in CRM-only exports)"],
11979
+ meaning: "Board question: what does a new logo cost to win?",
11980
+ expert_read: "Without spend, NTRP cannot invent CAC. Connect campaign spend or finance exports to unlock unit economics.",
11981
+ deepdive: [
11982
+ "Always unavailable on CRM-only demos. That is expected.",
11983
+ "Unlocks LTV:CAC, Payback, and Magic Number when spend lands."
11984
+ ],
11985
+ visual: { kind: "none", caption: "Needs campaign spend or a finance export" },
11986
+ audience: {
11987
+ board: "CAC is locked until spend data is connected. CRM alone cannot price acquisition.",
11988
+ ops: "Bring campaign or S&M spend. Until then, unit-econ metrics stay unavailable by design."
11989
+ },
11990
+ aliases: ["customer acquisition cost", "acquisition cost"]
11991
+ },
11992
+ {
11993
+ id: "ltv_cac_ratio",
11994
+ kind: "saas",
11995
+ label: "LTV:CAC Ratio",
11996
+ group: "Unit Economics",
11997
+ tagline: "Does acquisition spend return enough value?",
11998
+ how_computed: "LTV proxy divided by CAC. Unavailable without spend (CAC).",
11999
+ formula_lines: ["LTV:CAC = LTV_proxy / CAC", "(requires CAC)"],
12000
+ meaning: "Board question: do we earn enough lifetime value per dollar spent to acquire?",
12001
+ expert_read: "In the efficiency era, boards weigh LTV:CAC and payback as heavily as growth. Classic rule of thumb is 3x or more. Motion and gross margin matter.",
12002
+ deepdive: ["Blocked on CAC. See LTV Proxy and CAC."],
12003
+ visual: { kind: "none", caption: "This metric needs CAC (spend data)" },
12004
+ audience: {
12005
+ board: "LTV:CAC is the acquisition ROI story. It is available once spend is connected.",
12006
+ ops: "LTV_proxy / CAC. Unlocks with spend import."
12007
+ },
12008
+ aliases: ["ltv cac", "ltv/cac", "ltv to cac"]
12009
+ },
12010
+ {
12011
+ id: "payback_months",
12012
+ kind: "saas",
12013
+ label: "Payback Months",
12014
+ group: "Unit Economics",
12015
+ tagline: "How many months to recover CAC?",
12016
+ how_computed: "This metric requires CAC and spend. Lower is better.",
12017
+ formula_lines: ["Payback \u2248 CAC / (monthly gross profit per customer)", "(requires spend data)"],
12018
+ meaning: "Board question: how fast does acquisition spend return? Efficiency-era prior: under 18 months is often healthy.",
12019
+ expert_read: "Boards now weigh payback (under 18 months) as heavily as growth in many motions.",
12020
+ deepdive: ["Blocked on CAC. Benchmarks exist per motion once data lands."],
12021
+ visual: { kind: "none", caption: "This metric needs CAC (spend data)" },
12022
+ audience: {
12023
+ board: "Payback is how fast CAC returns. Efficiency-era boards often want under 18 months.",
12024
+ ops: "Requires CAC. Motion green and yellow bands apply when available."
12025
+ },
12026
+ aliases: ["payback", "cac payback"],
12027
+ benchmarkHint: (motion) => monthsBand(motion)
12028
+ },
12029
+ {
12030
+ id: "magic_number",
12031
+ kind: "saas",
12032
+ label: "Magic Number",
12033
+ group: "Unit Economics",
12034
+ tagline: "Sales efficiency: net new ARR per sales dollar.",
12035
+ how_computed: "This metric requires sales spend. Classic form: net new ARR (quarter) / prior-quarter S&M spend.",
12036
+ formula_lines: [
12037
+ "Magic Number \u2248 Net New ARR(q) / S&M spend(q\u22121)",
12038
+ "(requires spend data)"
12039
+ ],
12040
+ meaning: "Board question: how efficiently does sales spend produce net new ARR? Prior: above 0.75 is often healthy. Above 1 is strong.",
12041
+ expert_read: "In the efficiency era, a magic number above 0.75 is weighed alongside growth. Without spend, NTRP keeps this unavailable. NTRP does not invent it.",
12042
+ deepdive: ["Blocked on spend. Benchmarks per motion are ready when data lands."],
12043
+ visual: { kind: "none", caption: "This metric needs S&M spend data" },
12044
+ audience: {
12045
+ board: "Magic Number prices sales efficiency. It is available once S&M spend is connected.",
12046
+ ops: "Net new ARR / prior S&M. Motion benchmarks apply when spend lands."
12047
+ },
12048
+ aliases: ["sales magic number", "sales efficiency magic number"],
12049
+ benchmarkHint: (motion) => magicBand(motion)
12210
12050
  }
12211
- if (explainer.play_id) {
12212
- lines.push("");
12213
- lines.push(
12214
- chalk9.dim(" Play: ") + paint("accent", explainer.play_id)
12215
- );
12051
+ ];
12052
+ METRIC_DEFINITIONS = [...VITALS, ...SAAS];
12053
+ CORE_DECK_IDS = [
12054
+ "arr",
12055
+ "nrr",
12056
+ "grr",
12057
+ "pipeline_coverage",
12058
+ "win_rate",
12059
+ "pipeline_velocity",
12060
+ "freshness",
12061
+ "flow_rate",
12062
+ "drop_rate",
12063
+ "signal_to_noise",
12064
+ "thread_depth"
12065
+ ];
12066
+ BY_ID = new Map(METRIC_DEFINITIONS.map((m) => [m.id, m]));
12067
+ ALIAS_INDEX = (() => {
12068
+ const idx = /* @__PURE__ */ new Map();
12069
+ for (const m of METRIC_DEFINITIONS) {
12070
+ idx.set(m.id.toLowerCase(), m.id);
12071
+ idx.set(m.label.toLowerCase(), m.id);
12072
+ for (const a of m.aliases ?? []) {
12073
+ idx.set(a.toLowerCase(), m.id);
12074
+ }
12216
12075
  }
12217
- lines.push("");
12218
- }
12219
- } else if (opts.visualOverride) {
12220
- const visLines = renderVisual(opts.visualOverride, inner);
12221
- if (visLines.length) {
12222
- lines.push(...visLines);
12223
- lines.push("");
12224
- }
12225
- }
12226
- if (opts.extraLines?.length) {
12227
- for (const line of opts.extraLines) {
12228
- if (line === "") lines.push("");
12229
- else pushWrapped(lines, line, inner);
12230
- }
12076
+ idx.set("signal-to-noise", "signal_to_noise");
12077
+ idx.set("signal:noise", "signal_to_noise");
12078
+ idx.set("flow-rate", "flow_rate");
12079
+ idx.set("drop-rate", "drop_rate");
12080
+ idx.set("thread-depth", "thread_depth");
12081
+ return idx;
12082
+ })();
12083
+ SAAS_METRIC_IDS = SAAS.map((m) => m.id);
12084
+ GATING_LAYER_VISUAL = {
12085
+ kind: "layer_stack",
12086
+ caption: "Gating order. First red in layer order wins. If none, first yellow. If none, lowest score.",
12087
+ layers: [
12088
+ { label: "L1 Freshness", highlight: true },
12089
+ { label: "L2 Flow Rate \xB7 Drop Rate" },
12090
+ { label: "L3 Signal:Noise" },
12091
+ { label: "L4 Thread Depth" }
12092
+ ]
12093
+ };
12231
12094
  }
12232
- return { title, lines, width, inner };
12095
+ });
12096
+
12097
+ // src/data/guide-slides.ts
12098
+ function normalize(raw) {
12099
+ return raw.trim().toLowerCase().replace(/\s+/g, "-").replace(/_/g, "-");
12233
12100
  }
12234
- function renderMetricSlide(explainer, opts = {}) {
12235
- const { title, lines, width, inner } = buildSlideContent(explainer, opts);
12236
- const border = (s) => paint("border", s);
12237
- const termW = termWidth();
12238
- const outerPad = " ".repeat(Math.max(0, Math.floor((termW - width) / 2)));
12239
- const out = [];
12240
- out.push("");
12241
- out.push(`${outerPad}${border(`\u256D${"\u2500".repeat(width - 2)}\u256E`)}`);
12242
- out.push(
12243
- `${outerPad}${border("\u2502 ")}${padRight(sectionHeading(title), inner)}${border(" \u2502")}`
12244
- );
12245
- out.push(`${outerPad}${border(`\u251C${"\u2500".repeat(width - 2)}\u2524`)}`);
12246
- for (const row of lines) {
12247
- out.push(
12248
- `${outerPad}${border("\u2502 ")}${padRight(truncateVisible(row, inner), inner)}${border(" \u2502")}`
12249
- );
12250
- }
12251
- const footer = opts.footer ?? (opts.deepdive ? `\u23CE next \xB7 b back \xB7 q quit` : `\u23CE next \xB7 /deepdive more \xB7 b back \xB7 q quit`);
12252
- out.push(`${outerPad}${border(`\u251C${"\u2500".repeat(width - 2)}\u2524`)}`);
12253
- out.push(
12254
- `${outerPad}${border("\u2502 ")}${padRight(chalk9.dim(truncateVisible(footer, inner)), inner)}${border(" \u2502")}`
12255
- );
12256
- out.push(`${outerPad}${border(`\u2570${"\u2500".repeat(width - 2)}\u256F`)}`);
12257
- out.push("");
12258
- if (opts.asLines) return out;
12259
- for (const line of out) console.log(line);
12101
+ function isGuideSectionQuery(raw) {
12102
+ const q = normalize(raw);
12103
+ return GUIDE_SECTION_ALIASES.includes(q);
12260
12104
  }
12261
- function progressDots(index, total) {
12262
- const parts = [];
12263
- for (let i = 1; i <= total; i++) {
12264
- parts.push(i === index ? paint("accent", "\u25CF") : chalk9.dim("\u25CB"));
12105
+ function resolveGuideId(raw) {
12106
+ const q = normalize(raw);
12107
+ if (!q) return null;
12108
+ if (isGuideSectionQuery(q)) return GUIDE_DECK_IDS[0];
12109
+ const underscored = q.replace(/-/g, "_");
12110
+ if (GUIDE_DECK_IDS.includes(underscored)) {
12111
+ return underscored;
12265
12112
  }
12266
- return parts.join("");
12267
- }
12268
- function printExplainerCatalogLine(explainer) {
12269
- const kind = explainer.kind === "vital" ? paint("accent", "vital") : chalk9.dim("saas ");
12270
- console.log(
12271
- ` ${kind} ${bold(explainer.id.padEnd(20))} ${chalk9.dim(explainer.label)} \u2014 ${chalk9.dim(explainer.tagline)}`
12272
- );
12113
+ return SLIDE_ALIASES[q] ?? SLIDE_ALIASES[underscored] ?? null;
12273
12114
  }
12274
- function printGuideCatalogLine(slide) {
12275
- console.log(
12276
- ` ${paint("accent", "how ")} ${bold(slide.id.padEnd(20))} ${chalk9.dim(slide.label)} \u2014 ${chalk9.dim(slide.tagline)}`
12277
- );
12115
+ function getGuideSlide(id) {
12116
+ return BY_ID2.get(id);
12278
12117
  }
12279
- function printDeepdiveHint(metricId, label) {
12280
- const name = label ?? metricId;
12281
- console.log(
12282
- " " + chalk9.dim("How this number works: ") + paint("accent", `/deepdive ${metricId}`) + chalk9.dim(` \u2014 ${name}`)
12283
- );
12284
- console.log();
12118
+ function listGuideSlides() {
12119
+ return GUIDE_DECK_IDS.map((id) => BY_ID2.get(id));
12285
12120
  }
12286
- var init_slides = __esm({
12287
- "src/ui/slides.ts"() {
12121
+ var GUIDE_DECK_IDS, GUIDE_SECTION_ALIASES, SLIDE_ALIASES, TALK, ASK, HANDOFF, LOOP, BY_ID2;
12122
+ var init_guide_slides = __esm({
12123
+ "src/data/guide-slides.ts"() {
12288
12124
  "use strict";
12289
- init_theme();
12290
- init_layout();
12125
+ GUIDE_DECK_IDS = ["talk", "ask", "handoff", "loop"];
12126
+ GUIDE_SECTION_ALIASES = [
12127
+ "guide",
12128
+ "how",
12129
+ "howto",
12130
+ "how-to",
12131
+ "how_to",
12132
+ "use",
12133
+ "using",
12134
+ "nav",
12135
+ "navigate",
12136
+ "userflow",
12137
+ "user-guide",
12138
+ "userguide"
12139
+ ];
12140
+ SLIDE_ALIASES = {
12141
+ talk: "talk",
12142
+ chat: "talk",
12143
+ type: "talk",
12144
+ speak: "talk",
12145
+ conversation: "talk",
12146
+ ask: "ask",
12147
+ connect: "ask",
12148
+ glossary: "ask",
12149
+ keyless: "ask",
12150
+ enter: "ask",
12151
+ definitions: "ask",
12152
+ handoff: "handoff",
12153
+ inbox: "handoff",
12154
+ skill: "handoff",
12155
+ claude: "handoff",
12156
+ ship: "handoff",
12157
+ pickup: "handoff",
12158
+ chatgpt: "handoff",
12159
+ cursor: "handoff",
12160
+ export: "handoff",
12161
+ loop: "loop",
12162
+ strategy: "loop",
12163
+ strategist: "loop",
12164
+ playbook: "loop",
12165
+ remember: "loop",
12166
+ sessions: "loop",
12167
+ progress: "loop"
12168
+ };
12169
+ TALK = {
12170
+ id: "talk",
12171
+ label: "Talk to NTRP",
12172
+ tagline: "Type English. Confirm the scope. Load data. Then listen.",
12173
+ visual: {
12174
+ kind: "funnel",
12175
+ caption: "Bare Enter submits the dim \u23CE hint at these gates. If nothing is armed, Enter does nothing.",
12176
+ funnel: [
12177
+ { label: "Type a question", widthPct: 100 },
12178
+ { label: "\u23CE yes (scope)", widthPct: 78 },
12179
+ { label: "Load data", widthPct: 56 },
12180
+ { label: "\u23CE go ahead", widthPct: 34 }
12181
+ ]
12182
+ },
12183
+ lines: [
12184
+ 'You do not need a slash. Type the question you need. Examples: "is our retention real for the board?" or "pipeline health".',
12185
+ "",
12186
+ "NTRP restates the question as a scope card. Confirm with \u23CE yes, or type yes. NTRP listens. NTRP does not invent a different question.",
12187
+ "",
12188
+ "When the dataset is empty, type \u23CE use demo data. Or paste a CSV path. Or type /ingest. When the gap card shows that the formulas can compute, type \u23CE go ahead.",
12189
+ "",
12190
+ "Vital signs and SaaS metrics compute without an AI key. Every score that has a dollar translation shows it."
12191
+ ],
12192
+ deepdive: [
12193
+ "Power-user slash commands still work. They stay hidden from /help: /new, /diagnose, /metrics, /ingest, /session.",
12194
+ 'Type "use demo data" to load the hidden_crisis scenario. Company profile is optional. In scripts, type /demo --no-profile.',
12195
+ 'After compute, the prompt becomes ask \u203A. Brief is the default depth. Type "go deep" when you want the long read.',
12196
+ "Type /home to show phase status. Type /help to list conversation shortcuts. /help does not list every command."
12197
+ ]
12198
+ };
12199
+ ASK = {
12200
+ id: "ask",
12201
+ label: "After the numbers",
12202
+ tagline: 'Glossary is free. "Our ARR" is compute. Narrative needs /connect.',
12203
+ visual: {
12204
+ kind: "split",
12205
+ caption: "Possessives such as our, my, and the team's skip the glossary. They go to compute or scope.",
12206
+ bars: [
12207
+ { label: "what is ARR?", value: 55, tone: "accent" },
12208
+ { label: "our ARR", value: 45, tone: "neutral" }
12209
+ ]
12210
+ },
12211
+ lines: [
12212
+ '"what is ARR?" and "how is freshness calculated?" answer from the built-in glossary. No key. No data.',
12213
+ "",
12214
+ '"what is our ARR?" and "why is this red?" need a loaded dataset. AI findings and /ask need a stored key. Type /connect and paste any provider key. NTRP detects it.',
12215
+ "",
12216
+ "Type /deepdive to replay this tour. Type /deepdive freshness for one slide. You can also type nrr, arr, or another id. Type /deepdive list for the catalog. Type /deepdive guide for this how-to section.",
12217
+ "",
12218
+ "When the prompt shows a dim \u23CE hint, bare Enter submits that action. Examples: \u23CE yes, \u23CE use demo data, \u23CE go ahead, \u23CE /connect. If there is no hint, Enter does nothing."
12219
+ ],
12220
+ deepdive: [
12221
+ "Type /connect ollama for a keyless local model. Type /connect --base-url <url> --id <name> for any OpenAI-compatible endpoint.",
12222
+ "Type /model refresh to re-discover models. A retired model self-heals on the first 404.",
12223
+ "First-run skip of this tour does not complete it. A home \u2691 chip can bring you back after the first analysis.",
12224
+ "Tab completes /deepdive <metric>. Finding cards and playbook triggers also link here."
12225
+ ]
12226
+ };
12227
+ HANDOFF = {
12228
+ id: "handoff",
12229
+ label: "Ship work to Claude",
12230
+ tagline: "Teach the inbox once. Later, tell Claude to pick it up.",
12231
+ visual: {
12232
+ kind: "layer_stack",
12233
+ caption: "You and Claude find and open the file. NTRP only writes.",
12234
+ layers: [
12235
+ { label: "Set a pickup folder (demo, onboard, or /inbox set)" },
12236
+ { label: "Paste the finder skill once (/inbox skill)", highlight: true },
12237
+ { label: "/handoff writes files. There is no paste block." },
12238
+ { label: '"Pick up the latest NTRP handoff."' }
12239
+ ]
12240
+ },
12241
+ lines: [
12242
+ 'Type "ship a board deck" or type /handoff. Files land in ~/Documents/Claude/ntrp-inbox by default. Type /inbox set to change the folder.',
12243
+ "",
12244
+ "During demo or company setup, or any time, type /inbox skill. Paste a standing finder into Claude, ChatGPT, or Cursor once. That skill tells the tool to follow latest-handoff.md and INDEX.md.",
12245
+ "",
12246
+ 'After that, each /handoff prints "Handoff ready". You will not get a paste block every write. Tell Claude: pick up the latest NTRP handoff.',
12247
+ "",
12248
+ "If you skipped this step, NTRP asks once when you load your own data. Skip then, and type /inbox set ~/Documents/Claude/ntrp-inbox. Then type /inbox skill. Type /handoff skill to reprint the same finder."
12249
+ ],
12250
+ deepdive: [
12251
+ "Inbox copies: latest-handoff.md, latest-pickup.md, SKILL.md, INDEX.md. Dated files also live under ~/.ntrp/exports/.",
12252
+ "Type /exports to list writes. Type /inbox show to print the folder and the latest pointer. Type /handoff --print to show the prompt body in the terminal.",
12253
+ "Audience-framed Metric definitions append to decks and reports. Claude then has the same glossary you walked.",
12254
+ "Type /end to close a session. NTRP writes a transcript plus a 1-page context brief under ~/.ntrp/sessions/."
12255
+ ]
12256
+ };
12257
+ LOOP = {
12258
+ id: "loop",
12259
+ label: "Stay in the loop",
12260
+ tagline: "Diagnose \u2192 plan \u2192 review \u2192 remember. NTRP learns your business.",
12261
+ visual: {
12262
+ kind: "layer_stack",
12263
+ caption: "Stethoscope, not hospital. Observe and recommend. Never prescribe surgery.",
12264
+ layers: [
12265
+ { label: "Listen (/diagnose, /metrics)" },
12266
+ { label: 'Plan ("how should we fix this?" / /strategy)', highlight: true },
12267
+ { label: "Review (/strategy review, /playbook)" },
12268
+ { label: "Remember (/remember, /rate, ANALYST.md)" }
12269
+ ]
12270
+ },
12271
+ lines: [
12272
+ 'Type "how should we fix this?" or type /strategy. NTRP builds a measurable plan with milestones and dollar-anchored ranges. Type /strategy review to check those against later vital signs.',
12273
+ "",
12274
+ "When a vital sign is red, type /playbook. NTRP names the matching play. Outcomes from reviews annotate the catalog with what hit here.",
12275
+ "",
12276
+ "Type /remember to store a durable fact. Type /rate bad <reason> to write a calibration. Optional ~/.ntrp/ANALYST.md is standing voice and priorities. It never overrides safety rules.",
12277
+ "",
12278
+ "Commands: /sessions show, /session <id> pickup, /end (transcript plus brief), /home, /progress, /help."
12279
+ ],
12280
+ deepdive: [
12281
+ 'Type "build me a game plan" before analysis. NTRP queues the strategist and resumes after compute.',
12282
+ "Interactive sessions distill 5 or fewer durable facts on close when an LLM key is stored. One-shot commands do not bank hours or distill.",
12283
+ "Type /scratch to factory-reset data and config. progress.json and install.json survive unless you pass --include-progress.",
12284
+ "Credits in /progress accrue in interactive ntrp only."
12285
+ ]
12286
+ };
12287
+ BY_ID2 = /* @__PURE__ */ new Map([
12288
+ [TALK.id, TALK],
12289
+ [ASK.id, ASK],
12290
+ [HANDOFF.id, HANDOFF],
12291
+ [LOOP.id, LOOP]
12292
+ ]);
12291
12293
  }
12292
12294
  });
12293
12295
 
@@ -12296,6 +12298,7 @@ var metric_tour_exports = {};
12296
12298
  __export(metric_tour_exports, {
12297
12299
  METRICS_TOUR_MILESTONE_ID: () => METRICS_TOUR_MILESTONE_ID,
12298
12300
  describeTourDeck: () => describeTourDeck,
12301
+ firstRunTourDefaultChoice: () => firstRunTourDefaultChoice,
12299
12302
  getDeepdiveNudge: () => getDeepdiveNudge,
12300
12303
  hasAnalysisForDeepdiveNudge: () => hasAnalysisForDeepdiveNudge,
12301
12304
  hasCompletedMetricsTour: () => hasCompletedMetricsTour,
@@ -12343,6 +12346,7 @@ function hasAnalysisForDeepdiveNudge(ctx) {
12343
12346
  }
12344
12347
  }
12345
12348
  function getDeepdiveNudge(ctx) {
12349
+ if (!hasValidLicense()) return null;
12346
12350
  if (hasCompletedMetricsTour() || hasSeenDeepdiveHomeNudge()) return null;
12347
12351
  if (!hasAnalysisForDeepdiveNudge(ctx)) return null;
12348
12352
  return {
@@ -12622,6 +12626,9 @@ function printGuideCard(slideId, opts = {}) {
12622
12626
  });
12623
12627
  return true;
12624
12628
  }
12629
+ function firstRunTourDefaultChoice() {
12630
+ return loadProgress().milestones_unlocked.includes(METRICS_TOUR_MILESTONE_ID) ? "skip" : "tour";
12631
+ }
12625
12632
  async function offerFirstRunTour(ctx) {
12626
12633
  if (hasCompletedMetricsTour()) return false;
12627
12634
  console.log();
@@ -12629,6 +12636,10 @@ async function offerFirstRunTour(ctx) {
12629
12636
  console.log(
12630
12637
  " " + chalk10.dim("SaaS numbers, five vitals, then how to talk to NTRP and ship work to Claude.")
12631
12638
  );
12639
+ const tourDefault = firstRunTourDefaultChoice();
12640
+ if (tourDefault === "skip") {
12641
+ console.log(" " + chalk10.dim("You have taken this tour before."));
12642
+ }
12632
12643
  console.log();
12633
12644
  const session = createPromptSession(ctx.rl, ctx);
12634
12645
  try {
@@ -12646,7 +12657,7 @@ async function offerFirstRunTour(ctx) {
12646
12657
  description: "Re-open anytime with /deepdive or /deepdive guide"
12647
12658
  }
12648
12659
  ],
12649
- { default: "tour" }
12660
+ { default: tourDefault }
12650
12661
  );
12651
12662
  if (choice === "skip") {
12652
12663
  markMetricsTourSkipped();
@@ -12671,6 +12682,7 @@ var init_metric_tour = __esm({
12671
12682
  init_prompts();
12672
12683
  init_store();
12673
12684
  init_progress();
12685
+ init_activation();
12674
12686
  init_profile();
12675
12687
  init_metric_definitions();
12676
12688
  init_guide_slides();
@@ -15284,7 +15296,7 @@ var init_explore_mode = __esm({
15284
15296
 
15285
15297
  // src/conversation/recommended-action.ts
15286
15298
  function resolveRecommendedAction(ctx) {
15287
- if (!hasValidLicense()) return { submit: "/checkout", hint: "/checkout" };
15299
+ if (!hasValidLicense()) return { submit: "/activate", hint: "/activate" };
15288
15300
  const phase = resolveConversationPhase(ctx);
15289
15301
  switch (phase) {
15290
15302
  case "explore":
@@ -15345,6 +15357,9 @@ function consumeOrientEmptyEnterCoach(ctx) {
15345
15357
  if (resolveConversationPhase(ctx) !== "orient") return null;
15346
15358
  if (ctx.orientEmptyEnterSeen) return null;
15347
15359
  ctx.orientEmptyEnterSeen = true;
15360
+ if (!hasValidLicense()) {
15361
+ return "Type /activate to paste a key. Type /checkout if you need to sign up.";
15362
+ }
15348
15363
  return "Type a question, type use demo data, or type /deepdive. Enter alone does not start a step here.";
15349
15364
  }
15350
15365
  function formatPhaseLabel(phase) {
@@ -15405,6 +15420,7 @@ var init_phase = __esm({
15405
15420
  init_explore_mode();
15406
15421
  init_session_state();
15407
15422
  init_theme();
15423
+ init_activation();
15408
15424
  init_recommended_action();
15409
15425
  PROMPT_LABELS = {
15410
15426
  orient: "\u203A",
@@ -19738,9 +19754,11 @@ var inbox_setup_exports = {};
19738
19754
  __export(inbox_setup_exports, {
19739
19755
  maybeOfferInboxOnProduction: () => maybeOfferInboxOnProduction,
19740
19756
  offerInboxSkillSetup: () => offerInboxSkillSetup,
19757
+ reuseInboxFolderIfPresent: () => reuseInboxFolderIfPresent,
19741
19758
  shouldOfferInboxSkillSetup: () => shouldOfferInboxSkillSetup
19742
19759
  });
19743
19760
  import chalk19 from "chalk";
19761
+ import { existsSync as existsSync22 } from "fs";
19744
19762
  function markDemoOffered() {
19745
19763
  setConfigValue("ai-inbox-nudge-seen", "true");
19746
19764
  }
@@ -19769,6 +19787,23 @@ function printSkipHint(beat) {
19769
19787
  " " + chalk19.dim("Skipped. NTRP will not ask again. Type ") + setCmd + chalk19.dim(" then ") + skillCmd + chalk19.dim(" at any time.")
19770
19788
  );
19771
19789
  }
19790
+ async function reuseInboxFolderIfPresent(session, beat, folderPath = defaultAiInboxDir()) {
19791
+ if (getAiInboxDir()) return false;
19792
+ if (!existsSync22(folderPath)) return false;
19793
+ console.log(" " + chalk19.dim("Pickup folder still on disk: ") + folderPath);
19794
+ const reuse = await session.confirm("Reuse this pickup folder?", true);
19795
+ if (!reuse) return false;
19796
+ const resolved = setAiInboxDir(folderPath);
19797
+ markDemoOffered();
19798
+ if (beat === "production") markProductionOffered();
19799
+ console.log();
19800
+ console.log(" " + paint("accent", "Inbox ready") + chalk19.dim(" ") + resolved);
19801
+ console.log(
19802
+ " " + chalk19.dim("Folder reused. Type ") + paint("accent", "/inbox skill") + chalk19.dim(" to print the finder again.")
19803
+ );
19804
+ console.log();
19805
+ return true;
19806
+ }
19772
19807
  async function offerInboxSkillSetup(session, opts = {}) {
19773
19808
  const beat = opts.beat ?? "production";
19774
19809
  if (!shouldOfferInboxSkillSetup(beat)) return;
@@ -19783,6 +19818,7 @@ async function offerInboxSkillSetup(session, opts = {}) {
19783
19818
  console.log(" " + chalk19.dim("You skipped this during demo."));
19784
19819
  }
19785
19820
  console.log();
19821
+ if (await reuseInboxFolderIfPresent(session, beat)) return;
19786
19822
  const want = await session.confirm("Set a pickup folder for Claude, ChatGPT, or Cursor?", true);
19787
19823
  if (!want) {
19788
19824
  if (beat === "demo") markDemoOffered();
@@ -19853,7 +19889,7 @@ __export(ingest_exports, {
19853
19889
  handler: () => handler2
19854
19890
  });
19855
19891
  import chalk20 from "chalk";
19856
- import { readFileSync as readFileSync19, existsSync as existsSync22 } from "fs";
19892
+ import { readFileSync as readFileSync19, existsSync as existsSync23 } from "fs";
19857
19893
  import { basename as basename6 } from "path";
19858
19894
  async function handler2(args, ctx) {
19859
19895
  const { positional, flags } = parseArgs2(args, [
@@ -19877,7 +19913,7 @@ async function handler2(args, ctx) {
19877
19913
  console.error(chalk20.dim(" /ingest --demo [--scenario <name>]"));
19878
19914
  process.exit(1);
19879
19915
  }
19880
- if (!existsSync22(file)) {
19916
+ if (!existsSync23(file)) {
19881
19917
  console.error(chalk20.red(` File not found: ${file}`));
19882
19918
  process.exit(1);
19883
19919
  }
@@ -21783,9 +21819,9 @@ async function handleGetSessionBrief(input) {
21783
21819
  if (!target) {
21784
21820
  return { error: `No session matching "${raw}".` };
21785
21821
  }
21786
- const { existsSync: existsSync33, readFileSync: readFileSync24 } = await import("fs");
21822
+ const { existsSync: existsSync34, readFileSync: readFileSync24 } = await import("fs");
21787
21823
  const briefPath = contextDocPathForSession2(target.id);
21788
- if (!existsSync33(briefPath)) {
21824
+ if (!existsSync34(briefPath)) {
21789
21825
  return {
21790
21826
  session_id: target.id,
21791
21827
  error: "No context brief on disk for this session (created before brief storage existed).",
@@ -24192,7 +24228,7 @@ __export(new_exports, {
24192
24228
  handler: () => handler6
24193
24229
  });
24194
24230
  import chalk25 from "chalk";
24195
- import { existsSync as existsSync23 } from "fs";
24231
+ import { existsSync as existsSync24 } from "fs";
24196
24232
  import { basename as basename7 } from "path";
24197
24233
  async function handler6(args, ctx) {
24198
24234
  const { positional, flags } = parseArgs2(args, ["demo", "empty", "list-scenarios", "regen-taxonomy"]);
@@ -24214,7 +24250,7 @@ async function handler6(args, ctx) {
24214
24250
  console.error(chalk25.red(" Usage: /new <file.csv> | --demo [--scenario <name>] | --empty [--lens health|metrics]"));
24215
24251
  return;
24216
24252
  }
24217
- if (source.kind === "file" && !existsSync23(source.path)) {
24253
+ if (source.kind === "file" && !existsSync24(source.path)) {
24218
24254
  console.error(chalk25.red(` File not found: ${source.path}`));
24219
24255
  return;
24220
24256
  }
@@ -24441,7 +24477,7 @@ __export(end_exports, {
24441
24477
  handler: () => handler7
24442
24478
  });
24443
24479
  import chalk26 from "chalk";
24444
- import { existsSync as existsSync24 } from "fs";
24480
+ import { existsSync as existsSync25 } from "fs";
24445
24481
  async function handler7(args, ctx) {
24446
24482
  if (args.length > 0) {
24447
24483
  console.error(chalk26.red(" Usage: /end"));
@@ -24478,10 +24514,10 @@ async function handler7(args, ctx) {
24478
24514
  if (summary) {
24479
24515
  console.log(" " + chalk26.dim(summary));
24480
24516
  }
24481
- if (existsSync24(transcriptPathForSession(endedId))) {
24517
+ if (existsSync25(transcriptPathForSession(endedId))) {
24482
24518
  console.log(" " + chalk26.dim("Transcript: ") + chalk26.dim(transcriptPathForSession(endedId)));
24483
24519
  }
24484
- if (existsSync24(contextDocPathForSession(endedId))) {
24520
+ if (existsSync25(contextDocPathForSession(endedId))) {
24485
24521
  console.log(" " + chalk26.dim("Context brief: ") + chalk26.dim(contextDocPathForSession(endedId)));
24486
24522
  }
24487
24523
  console.log();
@@ -24503,7 +24539,7 @@ __export(session_exports, {
24503
24539
  });
24504
24540
  import chalk27 from "chalk";
24505
24541
  import { join as join23 } from "path";
24506
- import { existsSync as existsSync25 } from "fs";
24542
+ import { existsSync as existsSync26 } from "fs";
24507
24543
  async function handler8(args, ctx) {
24508
24544
  const sub = args[0];
24509
24545
  if (!sub) return listSessionsView(ctx);
@@ -24641,7 +24677,7 @@ async function pickUp(idArg, ctx) {
24641
24677
  );
24642
24678
  }
24643
24679
  const contextPath = contextDocPathForSession(target.id);
24644
- if (existsSync25(contextPath)) {
24680
+ if (existsSync26(contextPath)) {
24645
24681
  console.log(" " + chalk27.dim("Context brief: ") + chalk27.dim(contextPath));
24646
24682
  }
24647
24683
  console.log();
@@ -30053,13 +30089,36 @@ __export(activate_exports, {
30053
30089
  handler: () => handler23
30054
30090
  });
30055
30091
  import chalk45 from "chalk";
30092
+ async function afterLicenseSuccess(ctx) {
30093
+ if (ctx.oneShot) return;
30094
+ const { resumeSetupAfterLicense: resumeSetupAfterLicense2 } = await Promise.resolve().then(() => (init_first_run(), first_run_exports));
30095
+ await resumeSetupAfterLicense2(ctx);
30096
+ }
30056
30097
  async function handler23(args, ctx) {
30057
30098
  const { positional } = parseArgs2(args);
30058
30099
  const key = positional[0];
30059
30100
  if (!key) {
30060
- console.error(chalk45.red("\n Usage: /activate <key>"));
30061
- console.error(chalk45.dim(" Or type /upgrade for checkout + paste flow.\n"));
30062
- if (ctx.oneShot) process.exit(1);
30101
+ if (ctx.oneShot || !process.stdin.isTTY) {
30102
+ console.error(chalk45.red("\n Usage: /activate <key>"));
30103
+ console.error(chalk45.dim(" Or type /activate in ntrp to paste a key.\n"));
30104
+ if (ctx.oneShot) process.exit(1);
30105
+ return;
30106
+ }
30107
+ if (hasValidLicense()) {
30108
+ console.log();
30109
+ console.log(chalk45.green(" A license is already active."));
30110
+ console.log();
30111
+ await afterLicenseSuccess(ctx);
30112
+ return;
30113
+ }
30114
+ console.log();
30115
+ console.log(
30116
+ " " + chalk45.dim("Paste a trial key or a Pro key. Type ") + paint("accent", "/checkout") + chalk45.dim(" if you need to sign up.")
30117
+ );
30118
+ console.log();
30119
+ const activated = await promptForLicenseKey(ctx);
30120
+ if (!activated) return;
30121
+ await afterLicenseSuccess(ctx);
30063
30122
  return;
30064
30123
  }
30065
30124
  try {
@@ -30074,10 +30133,7 @@ async function handler23(args, ctx) {
30074
30133
  console.log(chalk45.green(`
30075
30134
  License activated: ${result.message}
30076
30135
  `));
30077
- if (!ctx.oneShot) {
30078
- const { replayPendingBlockedLine: replayPendingBlockedLine2 } = await Promise.resolve().then(() => (init_dispatch(), dispatch_exports));
30079
- await replayPendingBlockedLine2(ctx);
30080
- }
30136
+ await afterLicenseSuccess(ctx);
30081
30137
  } catch (err) {
30082
30138
  const message = err instanceof Error ? err.message : "License activation failed";
30083
30139
  console.error(chalk45.red(`
@@ -30092,6 +30148,9 @@ var init_activate = __esm({
30092
30148
  "use strict";
30093
30149
  init_verify();
30094
30150
  init_argparse();
30151
+ init_activation();
30152
+ init_upgrade();
30153
+ init_theme();
30095
30154
  }
30096
30155
  });
30097
30156
 
@@ -30130,8 +30189,8 @@ async function handler24(args, ctx) {
30130
30189
  return;
30131
30190
  }
30132
30191
  if (!ctx.oneShot) {
30133
- const { replayPendingBlockedLine: replayPendingBlockedLine2 } = await Promise.resolve().then(() => (init_dispatch(), dispatch_exports));
30134
- await replayPendingBlockedLine2(ctx);
30192
+ const { resumeSetupAfterLicense: resumeSetupAfterLicense2 } = await Promise.resolve().then(() => (init_first_run(), first_run_exports));
30193
+ await resumeSetupAfterLicense2(ctx);
30135
30194
  }
30136
30195
  }
30137
30196
  var init_upgrade2 = __esm({
@@ -30159,7 +30218,7 @@ var init_checkout = __esm({
30159
30218
  });
30160
30219
 
30161
30220
  // src/services/setup.ts
30162
- import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync20, writeFileSync as writeFileSync19 } from "fs";
30221
+ import { existsSync as existsSync27, mkdirSync as mkdirSync17, readFileSync as readFileSync20, writeFileSync as writeFileSync19 } from "fs";
30163
30222
  import { join as join29 } from "path";
30164
30223
  function setupCheck() {
30165
30224
  const home = ntrpHome();
@@ -32157,7 +32216,7 @@ __export(sessions_exports, {
32157
32216
  handler: () => handler35
32158
32217
  });
32159
32218
  import chalk61 from "chalk";
32160
- import { existsSync as existsSync27 } from "fs";
32219
+ import { existsSync as existsSync28 } from "fs";
32161
32220
  async function handler35(args, _ctx) {
32162
32221
  const sub = args[0] ?? "list";
32163
32222
  if (sub === "list" || !args[0]) {
@@ -32264,12 +32323,12 @@ function showSession(idArg) {
32264
32323
  console.log();
32265
32324
  const transcriptPath = transcriptPathForSession(session.id);
32266
32325
  const contextPath = contextDocPathForSession(session.id);
32267
- if (existsSync27(transcriptPath) || existsSync27(contextPath)) {
32326
+ if (existsSync28(transcriptPath) || existsSync28(contextPath)) {
32268
32327
  console.log(" " + chalk61.dim("\u2500".repeat(40)));
32269
- if (existsSync27(contextPath)) {
32328
+ if (existsSync28(contextPath)) {
32270
32329
  console.log(" " + chalk61.dim("Context brief: ") + chalk61.dim(contextPath));
32271
32330
  }
32272
- if (existsSync27(transcriptPath)) {
32331
+ if (existsSync28(transcriptPath)) {
32273
32332
  console.log(" " + chalk61.dim("Full transcript: ") + chalk61.dim(transcriptPath));
32274
32333
  }
32275
32334
  console.log();
@@ -33143,27 +33202,20 @@ var init_model = __esm({
33143
33202
  });
33144
33203
 
33145
33204
  // src/config/update-check.ts
33146
- var update_check_exports = {};
33147
- __export(update_check_exports, {
33148
- invalidateUpdateCheckCache: () => invalidateUpdateCheckCache,
33149
- isCacheFresh: () => isCacheFresh,
33150
- loadUpdateCheckCache: () => loadUpdateCheckCache,
33151
- saveUpdateCheckCache: () => saveUpdateCheckCache
33152
- });
33153
- import { existsSync as existsSync28, mkdirSync as mkdirSync18, readFileSync as readFileSync21, unlinkSync as unlinkSync5, writeFileSync as writeFileSync20 } from "fs";
33205
+ import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync21, unlinkSync as unlinkSync5, writeFileSync as writeFileSync20 } from "fs";
33154
33206
  import { join as join33 } from "path";
33155
33207
  function cachePath2() {
33156
33208
  return join33(ntrpHome(), "update-check.json");
33157
33209
  }
33158
33210
  function ensureDir7() {
33159
33211
  const dir = ntrpHome();
33160
- if (!existsSync28(dir)) {
33212
+ if (!existsSync29(dir)) {
33161
33213
  mkdirSync18(dir, { recursive: true });
33162
33214
  }
33163
33215
  }
33164
33216
  function loadUpdateCheckCache() {
33165
33217
  const path = cachePath2();
33166
- if (!existsSync28(path)) return null;
33218
+ if (!existsSync29(path)) return null;
33167
33219
  try {
33168
33220
  const parsed = JSON.parse(readFileSync21(path, "utf-8"));
33169
33221
  if (!parsed || typeof parsed !== "object" || typeof parsed.lastCheck !== "number" || typeof parsed.latestVersion !== "string") {
@@ -33184,7 +33236,7 @@ function isCacheFresh(cache2, ttlMs = CACHE_TTL_MS2) {
33184
33236
  }
33185
33237
  function invalidateUpdateCheckCache() {
33186
33238
  const path = cachePath2();
33187
- if (existsSync28(path)) {
33239
+ if (existsSync29(path)) {
33188
33240
  unlinkSync5(path);
33189
33241
  }
33190
33242
  }
@@ -33198,7 +33250,7 @@ var init_update_check = __esm({
33198
33250
  });
33199
33251
 
33200
33252
  // src/version.ts
33201
- import { existsSync as existsSync29, readFileSync as readFileSync22 } from "fs";
33253
+ import { existsSync as existsSync30, readFileSync as readFileSync22 } from "fs";
33202
33254
  import { dirname as dirname6, join as join34 } from "path";
33203
33255
  import { fileURLToPath } from "url";
33204
33256
  function getInstalledVersion() {
@@ -33206,7 +33258,7 @@ function getInstalledVersion() {
33206
33258
  const start = dirname6(fileURLToPath(import.meta.url));
33207
33259
  for (const rel of ["../package.json", "../../package.json"]) {
33208
33260
  const path = join34(start, rel);
33209
- if (!existsSync29(path)) continue;
33261
+ if (!existsSync30(path)) continue;
33210
33262
  try {
33211
33263
  const pkg = JSON.parse(readFileSync22(path, "utf-8"));
33212
33264
  if (typeof pkg.version === "string" && pkg.version.length > 0) {
@@ -33230,10 +33282,13 @@ var init_version = __esm({
33230
33282
  var registry_exports = {};
33231
33283
  __export(registry_exports, {
33232
33284
  NPM_PACKAGE: () => NPM_PACKAGE,
33285
+ applyUpdateCheckResult: () => applyUpdateCheckResult,
33233
33286
  checkForUpdate: () => checkForUpdate,
33234
33287
  fetchLatestVersion: () => fetchLatestVersion,
33235
33288
  formatUpdateNudge: () => formatUpdateNudge,
33236
- isNewerVersion: () => isNewerVersion
33289
+ hydrateUpdateAvailableFromCache: () => hydrateUpdateAvailableFromCache,
33290
+ isNewerVersion: () => isNewerVersion,
33291
+ startBackgroundUpdateCheck: () => startBackgroundUpdateCheck
33237
33292
  });
33238
33293
  function registryUrl() {
33239
33294
  return process.env.NTRP_REGISTRY_URL ?? "https://registry.npmjs.org/@sonnechasser/ntrp/latest";
@@ -33252,7 +33307,27 @@ function isNewerVersion(latest, current) {
33252
33307
  return lPatch > cPatch;
33253
33308
  }
33254
33309
  function formatUpdateNudge(current, latest) {
33255
- return `\u26A1 NTRP v${latest} available (you're on v${current}) \u2014 type /update to upgrade`;
33310
+ return `\u26A1 NTRP v${latest} available (running v${current}) \u2014 type /update`;
33311
+ }
33312
+ function hydrateUpdateAvailableFromCache(current) {
33313
+ const cached2 = loadUpdateCheckCache();
33314
+ if (!cached2?.latestVersion) return void 0;
33315
+ if (!isNewerVersion(cached2.latestVersion, current)) return void 0;
33316
+ return { current, latest: cached2.latestVersion };
33317
+ }
33318
+ function applyUpdateCheckResult(ctx, result) {
33319
+ if (result?.updateAvailable) {
33320
+ ctx.updateAvailable = { current: result.current, latest: result.latest };
33321
+ return;
33322
+ }
33323
+ if (result && !result.updateAvailable) {
33324
+ ctx.updateAvailable = void 0;
33325
+ }
33326
+ }
33327
+ function startBackgroundUpdateCheck(ctx) {
33328
+ const pending = checkForUpdate({ force: true, timeoutMs: 5e3 });
33329
+ ctx.pendingUpdateCheck = pending;
33330
+ void pending.then((result) => applyUpdateCheckResult(ctx, result)).catch(() => void 0);
33256
33331
  }
33257
33332
  async function fetchLatestVersion(timeoutMs = 5e3) {
33258
33333
  try {
@@ -33299,18 +33374,83 @@ var init_registry = __esm({
33299
33374
  }
33300
33375
  });
33301
33376
 
33377
+ // src/update/relaunch.ts
33378
+ var relaunch_exports = {};
33379
+ __export(relaunch_exports, {
33380
+ JUST_UPDATED_ENV: () => JUST_UPDATED_ENV,
33381
+ consumeJustUpdatedEnv: () => consumeJustUpdatedEnv,
33382
+ encodeJustUpdated: () => encodeJustUpdated,
33383
+ relaunchIntoHome: () => relaunchIntoHome,
33384
+ updateRestartSummary: () => updateRestartSummary
33385
+ });
33386
+ import { spawnSync } from "child_process";
33387
+ function encodeJustUpdated(fromVersion, toVersion) {
33388
+ return `${fromVersion}\u2192${toVersion}`;
33389
+ }
33390
+ function consumeJustUpdatedEnv() {
33391
+ const raw = process.env[JUST_UPDATED_ENV];
33392
+ if (!raw) return null;
33393
+ delete process.env[JUST_UPDATED_ENV];
33394
+ const sep5 = raw.includes("\u2192") ? "\u2192" : "->";
33395
+ const idx = raw.indexOf(sep5);
33396
+ if (idx <= 0) return null;
33397
+ const from = raw.slice(0, idx);
33398
+ const to = raw.slice(idx + sep5.length);
33399
+ if (!from || !to) return null;
33400
+ return { from, to };
33401
+ }
33402
+ function updateRestartSummary(toVersion) {
33403
+ return `Restart NTRP to use v${toVersion}`;
33404
+ }
33405
+ async function relaunchIntoHome(opts) {
33406
+ const { stopSessionTranscript: stopSessionTranscript2 } = await Promise.resolve().then(() => (init_transcript(), transcript_exports));
33407
+ const { close: close2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
33408
+ stopSessionTranscript2();
33409
+ await close2();
33410
+ try {
33411
+ opts.rl?.pause();
33412
+ } catch {
33413
+ }
33414
+ const result = spawnSync(process.execPath, process.argv.slice(1), {
33415
+ stdio: "inherit",
33416
+ env: {
33417
+ ...process.env,
33418
+ [JUST_UPDATED_ENV]: encodeJustUpdated(opts.fromVersion, opts.toVersion)
33419
+ }
33420
+ });
33421
+ if (result.error) {
33422
+ try {
33423
+ opts.rl?.resume();
33424
+ } catch {
33425
+ }
33426
+ return "failed";
33427
+ }
33428
+ process.exit(result.status ?? 0);
33429
+ return "failed";
33430
+ }
33431
+ var JUST_UPDATED_ENV;
33432
+ var init_relaunch = __esm({
33433
+ "src/update/relaunch.ts"() {
33434
+ "use strict";
33435
+ JUST_UPDATED_ENV = "NTRP_JUST_UPDATED";
33436
+ }
33437
+ });
33438
+
33302
33439
  // src/commands/update.ts
33303
33440
  var update_exports = {};
33304
33441
  __export(update_exports, {
33305
33442
  handler: () => handler43
33306
33443
  });
33307
- import { spawnSync } from "child_process";
33444
+ import { spawnSync as spawnSync2 } from "child_process";
33308
33445
  import chalk69 from "chalk";
33309
33446
  function tailLines(text, count = 5) {
33310
33447
  return text.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).slice(-count).join("\n");
33311
33448
  }
33312
33449
  function runGlobalInstall() {
33313
- const result = spawnSync(
33450
+ if (process.env.NTRP_UPDATE_NPM_STUB === "1") {
33451
+ return { ok: true, output: "stub" };
33452
+ }
33453
+ const result = spawnSync2(
33314
33454
  "npm",
33315
33455
  ["install", "-g", `${NPM_PACKAGE}@latest`],
33316
33456
  {
@@ -33321,7 +33461,7 @@ function runGlobalInstall() {
33321
33461
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
33322
33462
  return { ok: result.status === 0, output };
33323
33463
  }
33324
- async function handler43(_args, _ctx) {
33464
+ async function handler43(_args, ctx) {
33325
33465
  const current = getInstalledVersion();
33326
33466
  const latest = await fetchLatestVersion(1e4);
33327
33467
  if (!latest) {
@@ -33342,8 +33482,23 @@ async function handler43(_args, _ctx) {
33342
33482
  const { ok, output } = runGlobalInstall();
33343
33483
  if (ok) {
33344
33484
  invalidateUpdateCheckCache();
33345
- console.log(chalk69.green(` \u2713 Updated! Restart NTRP to use v${latest}`));
33485
+ if (ctx.oneShot) {
33486
+ console.log(chalk69.green(` \u2713 Updated! ${updateRestartSummary(latest)}`));
33487
+ console.log();
33488
+ return;
33489
+ }
33346
33490
  console.log();
33491
+ const failed = await relaunchIntoHome({
33492
+ fromVersion: current,
33493
+ toVersion: latest,
33494
+ rl: ctx.rl
33495
+ });
33496
+ if (failed) {
33497
+ const restart = updateRestartSummary(latest);
33498
+ console.log(chalk69.green(` \u2713 Updated! ${restart}`));
33499
+ console.log();
33500
+ return restart;
33501
+ }
33347
33502
  return;
33348
33503
  }
33349
33504
  const lower = output.toLowerCase();
@@ -33368,6 +33523,7 @@ var init_update = __esm({
33368
33523
  "use strict";
33369
33524
  init_update_check();
33370
33525
  init_registry();
33526
+ init_relaunch();
33371
33527
  init_version();
33372
33528
  PERMISSIONS_URL = "https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally";
33373
33529
  }
@@ -33680,7 +33836,7 @@ __export(exports_exports, {
33680
33836
  handler: () => handler46
33681
33837
  });
33682
33838
  import chalk73 from "chalk";
33683
- import { existsSync as existsSync30 } from "fs";
33839
+ import { existsSync as existsSync31 } from "fs";
33684
33840
  import { join as join35 } from "path";
33685
33841
  function usage3() {
33686
33842
  console.log(chalk73.dim(" Usage:"));
@@ -33839,7 +33995,7 @@ function runMove(args, ctx) {
33839
33995
  }
33840
33996
  try {
33841
33997
  const destDir = resolveUserPath(dest);
33842
- if (!existsSync30(destDir)) {
33998
+ if (!existsSync31(destDir)) {
33843
33999
  }
33844
34000
  const event = moveExport(idOrName, destDir);
33845
34001
  console.log();
@@ -34787,11 +34943,11 @@ Cross-provider IDs are rejected. Type \`/provider\` first to switch.`
34787
34943
  name: activate
34788
34944
  description: Enter a license key
34789
34945
  section: Settings
34790
- args: <license>
34946
+ args: [license]
34791
34947
  handler: ../commands/activate.ts
34792
34948
  ---
34793
34949
 
34794
- Activate NTRP with the key from your purchase email. Most commands need a valid license.`
34950
+ Activate NTRP with the key from your purchase email. Type \`/activate\` with no key to paste. Most commands need a valid license.`
34795
34951
  },
34796
34952
  {
34797
34953
  name: "upgrade",
@@ -34836,7 +34992,7 @@ Remaining nuances merge into a custom_context paragraph that flows into all AI s
34836
34992
  });
34837
34993
 
34838
34994
  // src/ai/prompt-parts.ts
34839
- import { existsSync as existsSync31, readFileSync as readFileSync23 } from "fs";
34995
+ import { existsSync as existsSync32, readFileSync as readFileSync23 } from "fs";
34840
34996
  import { join as join36 } from "path";
34841
34997
  function buildCompanyProfileBlock() {
34842
34998
  const p = loadProfile();
@@ -34857,7 +35013,7 @@ function buildCompanyProfileBlock() {
34857
35013
  function loadAnalystFile() {
34858
35014
  const path = join36(ntrpHome(), ANALYST_FILE_NAME);
34859
35015
  try {
34860
- if (!existsSync31(path)) return null;
35016
+ if (!existsSync32(path)) return null;
34861
35017
  const raw = sanitizeExternalText(readFileSync23(path, "utf-8").trim());
34862
35018
  if (!raw) return null;
34863
35019
  if (raw.length <= ANALYST_FILE_MAX_CHARS) return raw;
@@ -35375,7 +35531,7 @@ __export(ingest_chat_exports, {
35375
35531
  loadDemoFromChat: () => loadDemoFromChat,
35376
35532
  looksLikeFilePath: () => looksLikeFilePath
35377
35533
  });
35378
- import { existsSync as existsSync32 } from "fs";
35534
+ import { existsSync as existsSync33 } from "fs";
35379
35535
  import { basename as basename9, resolve as resolve9 } from "path";
35380
35536
  import { homedir as homedir8 } from "os";
35381
35537
  import chalk75 from "chalk";
@@ -35395,11 +35551,11 @@ function extractFilePath(input) {
35395
35551
  const m = trimmed.match(re);
35396
35552
  if (m?.[1]) {
35397
35553
  const p = expandPath(m[1]);
35398
- if (existsSync32(p)) return p;
35554
+ if (existsSync33(p)) return p;
35399
35555
  }
35400
35556
  if (!m?.[1] && re.test(trimmed) && trimmed.toLowerCase().endsWith(".csv")) {
35401
35557
  const p = expandPath(trimmed.replace(/^["']|["']$/g, ""));
35402
- if (existsSync32(p)) return p;
35558
+ if (existsSync33(p)) return p;
35403
35559
  }
35404
35560
  }
35405
35561
  return null;
@@ -35844,6 +36000,7 @@ __export(first_run_exports, {
35844
36000
  loadFirstRunDemo: () => loadFirstRunDemo,
35845
36001
  markFirstRunCompleted: () => markFirstRunCompleted,
35846
36002
  printFirstRunChip: () => printFirstRunChip,
36003
+ resumeSetupAfterLicense: () => resumeSetupAfterLicense,
35847
36004
  runFirstRunFork: () => runFirstRunFork,
35848
36005
  shouldOfferFirstRunFork: () => shouldOfferFirstRunFork
35849
36006
  });
@@ -35948,9 +36105,6 @@ async function completeInteractiveSetup(ctx, options = {}) {
35948
36105
  await loadFirstRunDemo(ctx, fork.scenario);
35949
36106
  }
35950
36107
  }
35951
- if (!isProfileConfigured()) {
35952
- printFirstRunChip();
35953
- }
35954
36108
  } catch (err) {
35955
36109
  if (err instanceof GlobalReplCommandError) {
35956
36110
  if (err.command === "exit") {
@@ -35966,14 +36120,22 @@ async function completeInteractiveSetup(ctx, options = {}) {
35966
36120
  await runGlobalAdminCommand2(err.command, `/${err.command}`, ctx);
35967
36121
  }
35968
36122
  markFirstRunCompleted();
35969
- if (!isProfileConfigured()) {
35970
- printFirstRunChip();
35971
- }
35972
36123
  return;
35973
36124
  }
35974
36125
  throw err;
35975
36126
  }
35976
36127
  }
36128
+ async function resumeSetupAfterLicense(ctx) {
36129
+ if (!hasValidLicense()) return false;
36130
+ if (!isProfileConfigured() && shouldOfferFirstRunFork()) {
36131
+ ctx.pendingBlockedLine = void 0;
36132
+ await completeInteractiveSetup(ctx, { skipBrand: true });
36133
+ return true;
36134
+ }
36135
+ const { replayPendingBlockedLine: replayPendingBlockedLine2 } = await Promise.resolve().then(() => (init_dispatch(), dispatch_exports));
36136
+ await replayPendingBlockedLine2(ctx);
36137
+ return false;
36138
+ }
35977
36139
  async function loadFirstRunDemo(ctx, scenario) {
35978
36140
  const { handler: demo } = await Promise.resolve().then(() => (init_generate(), generate_exports));
35979
36141
  const ok = await demo(["--no-profile", "--scenario", scenario, "--brief"], ctx);
@@ -36007,6 +36169,7 @@ var init_first_run = __esm({
36007
36169
  init_scenarios();
36008
36170
  init_repl_globals();
36009
36171
  init_global_admin();
36172
+ init_activation();
36010
36173
  FIRST_RUN_KEY = "first-run-completed";
36011
36174
  }
36012
36175
  });
@@ -36021,7 +36184,7 @@ function printScratchPreamble(includeProgress) {
36021
36184
  console.log();
36022
36185
  console.log(" " + chalk78.yellow.bold("This will permanently remove:"));
36023
36186
  console.log(" " + chalk78.dim(" \u2022 API key and all config.json settings"));
36024
- console.log(" " + chalk78.dim(" \u2022 Company profile (you'll re-onboard on next use)"));
36187
+ console.log(" " + chalk78.dim(" \u2022 Company profile (you will set up again in this session)"));
36025
36188
  console.log(" " + chalk78.dim(" \u2022 All sessions and datasets"));
36026
36189
  console.log(" " + chalk78.dim(" \u2022 Demo taxonomy cache"));
36027
36190
  if (includeProgress) {
@@ -36052,6 +36215,7 @@ function resetContextAfterScratch(ctx) {
36052
36215
  ctx.deliverIntent = false;
36053
36216
  ctx.computeInProgress = false;
36054
36217
  ctx.lastExchange = void 0;
36218
+ ctx.pendingBlockedLine = void 0;
36055
36219
  }
36056
36220
  async function handler48(args, ctx) {
36057
36221
  const { flags } = parseArgs2(args, ["confirm", "include-progress"]);
@@ -36071,18 +36235,17 @@ async function handler48(args, ctx) {
36071
36235
  resetContextAfterScratch(ctx);
36072
36236
  await rotateToFreshSession(ctx);
36073
36237
  await initSchema();
36074
- console.log();
36075
- const detail = includeProgress ? " \u2014 local config, data, and progress wiped." : " \u2014 local config and data wiped.";
36076
- console.log(" " + paint("accent", "\u2713 Scratch complete") + chalk78.dim(detail));
36077
- console.log();
36078
36238
  if (ctx.oneShot) {
36239
+ console.log();
36240
+ const detail = includeProgress ? " \u2014 local config, data, and progress wiped." : " \u2014 local config and data wiped.";
36241
+ console.log(" " + paint("accent", "\u2713 Scratch complete") + chalk78.dim(detail));
36242
+ console.log();
36079
36243
  console.log(
36080
36244
  " " + chalk78.dim("Run ") + paint("accent", "ntrp") + chalk78.dim(" interactively to complete onboarding.\n")
36081
36245
  );
36082
36246
  return "Scratch complete";
36083
36247
  }
36084
- console.log(" " + chalk78.dim("Starting onboarding..."));
36085
- console.log();
36248
+ clearSlideScreen();
36086
36249
  const shown = await ensureLicenseActivated(ctx, { exitOnCancel: false });
36087
36250
  if (!hasValidLicense()) {
36088
36251
  return "Scratch complete \u2014 license required";
@@ -36100,6 +36263,7 @@ var init_scratch = __esm({
36100
36263
  init_scratch_wipe();
36101
36264
  init_schema();
36102
36265
  init_theme();
36266
+ init_slides();
36103
36267
  init_activation();
36104
36268
  }
36105
36269
  });
@@ -36151,8 +36315,13 @@ function resolvePostAction(input) {
36151
36315
  if (cancelled || isCancelledSummary(summary)) return "none";
36152
36316
  if (command === "onboard" && !ctx.replStarted) return "none";
36153
36317
  if (HOME_COMMANDS.has(command)) return "home";
36318
+ if (command === "update" && isUpdateRestartFallback(summary)) return "home";
36154
36319
  return "none";
36155
36320
  }
36321
+ function isUpdateRestartFallback(summary) {
36322
+ if (!summary) return false;
36323
+ return /^Restart NTRP to use v/.test(summary);
36324
+ }
36156
36325
  var HOME_COMMANDS;
36157
36326
  var init_post_action = __esm({
36158
36327
  "src/cli/post-action.ts"() {
@@ -36605,11 +36774,26 @@ function formatHomeEntityCounts(counts) {
36605
36774
  if (acts > 0) parts.push(`${acts} ${acts === 1 ? "activity" : "activities"}`);
36606
36775
  return parts.join(" \xB7 ");
36607
36776
  }
36608
- function formatEmptyDataHomeHint(savedSessionCount) {
36777
+ function formatEmptyDataHomeHint(savedSessionCount, opts = {}) {
36778
+ const onboard = opts.includeOnboard === true ? chalk82.dim(", or ") + paint("accent", "/onboard") + chalk82.dim(" to calibrate") : "";
36609
36779
  if (savedSessionCount > 0) {
36610
- return chalk82.dim("Type ") + paint("accent", "use demo data") + chalk82.dim(" to load a sample. Type ") + paint("accent", "/session") + chalk82.dim(" to open a saved session");
36780
+ return chalk82.dim("Type ") + paint("accent", "use demo data") + chalk82.dim(" to load a sample") + onboard + chalk82.dim(". Type ") + paint("accent", "/session") + chalk82.dim(" to open a saved session");
36781
+ }
36782
+ return chalk82.dim("Type ") + paint("accent", "use demo data") + chalk82.dim(" to load a sample pipeline") + onboard;
36783
+ }
36784
+ function ntrpStatusRow(version, update) {
36785
+ if (update && isNewerVersion(update.latest, version)) {
36786
+ return {
36787
+ label: "ntrp",
36788
+ state: badge("UPDATE", "warning"),
36789
+ detail: `v${update.latest} \xB7 type /update`
36790
+ };
36611
36791
  }
36612
- return chalk82.dim("Type ") + paint("accent", "use demo data") + chalk82.dim(" to load a sample pipeline");
36792
+ return {
36793
+ label: "ntrp",
36794
+ state: chalk82.dim(`v${version}`),
36795
+ detail: ""
36796
+ };
36613
36797
  }
36614
36798
  function resolveSessionSummary(input) {
36615
36799
  if (input.scope?.intent_summary?.trim()) return input.scope.intent_summary.trim();
@@ -36665,7 +36849,7 @@ function formatActiveSessionLine(s, colW, ctx, opts) {
36665
36849
  function resolveWelcomeNextAction(input) {
36666
36850
  const { profileReady, hasData, ctx, unfinishedCount, licenseValid } = input;
36667
36851
  if (!licenseValid) {
36668
- return { label: "Next:", command: "/checkout", detail: "signup or paste a key" };
36852
+ return { label: "Next:", command: "/activate", detail: "paste a trial or Pro key" };
36669
36853
  }
36670
36854
  if (!profileReady && !hasData) {
36671
36855
  return { label: "Try:", command: "", detail: 'type what you want to investigate (e.g. "pipeline health")' };
@@ -36760,6 +36944,8 @@ function buildActiveSessionsLines(colW, ctx, activeSessions) {
36760
36944
  return lines;
36761
36945
  }
36762
36946
  async function printWelcome(ctx, version) {
36947
+ const updateForPaint = ctx.updateAvailable;
36948
+ ctx.updateHomePaintedLatest = updateForPaint?.latest;
36763
36949
  const width = termWidth();
36764
36950
  const cardW = resolveCardWidth({ min: 72, max: CARD_MAX_W, margin: CARD_SIDE_MARGIN * 2 });
36765
36951
  const innerW = cardW - 2;
@@ -36816,7 +37002,7 @@ async function printWelcome(ctx, version) {
36816
37002
  let licenseDetail;
36817
37003
  if (!license.valid) {
36818
37004
  licenseState = badge("NOT SET", "warning");
36819
- licenseDetail = "type /checkout";
37005
+ licenseDetail = "type /activate";
36820
37006
  } else if (license.edition === "trial" && license.trialPhase === "grace") {
36821
37007
  licenseState = badge("GRACE", "warning");
36822
37008
  licenseDetail = `${license.daysUntilLockout ?? 0} day${license.daysUntilLockout === 1 ? "" : "s"} left on trial`;
@@ -36832,6 +37018,7 @@ async function printWelcome(ctx, version) {
36832
37018
  licenseDetail = license.message;
36833
37019
  }
36834
37020
  const statusRows = [
37021
+ ntrpStatusRow(version, updateForPaint),
36835
37022
  {
36836
37023
  label: "license",
36837
37024
  state: licenseState,
@@ -36860,7 +37047,7 @@ async function printWelcome(ctx, version) {
36860
37047
  unfinishedCount: unfinishedSessions.length,
36861
37048
  licenseValid: license.valid
36862
37049
  });
36863
- const emptyDataHint = !hasData ? formatEmptyDataHomeHint(savedSessions.length) : null;
37050
+ const emptyDataHint = !hasData && license.valid ? formatEmptyDataHomeHint(savedSessions.length, { includeOnboard: !profileReady }) : null;
36864
37051
  const colW = useWideLayout ? leftW : contentW;
36865
37052
  const rightColW = useWideLayout ? rightW : contentW;
36866
37053
  const systemLines = buildSystemLines(colW, statusRows, recent);
@@ -36908,7 +37095,7 @@ async function printWelcome(ctx, version) {
36908
37095
  push(border(`\u2570${"\u2500".repeat(innerW)}\u256F`));
36909
37096
  push(
36910
37097
  truncateVisible(
36911
- ` ${paint("accent", "/help")}${chalk82.dim(" commands \xB7 ")}${paint("accent", "/deepdive")}${chalk82.dim(" tour \xB7 ")}${paint("accent", "/progress")}${chalk82.dim(" hours \xB7 ")}${paint("accent", "/session")}${chalk82.dim(" resume")}`,
37098
+ license.valid ? ` ${paint("accent", "/help")}${chalk82.dim(" commands \xB7 ")}${paint("accent", "/deepdive")}${chalk82.dim(" tour \xB7 ")}${paint("accent", "/progress")}${chalk82.dim(" hours \xB7 ")}${paint("accent", "/session")}${chalk82.dim(" resume")}` : ` ${paint("accent", "/help")}${chalk82.dim(" commands \xB7 ")}${paint("accent", "/activate")}${chalk82.dim(" paste a key \xB7 ")}${paint("accent", "/checkout")}${chalk82.dim(" signup \xB7 ")}${paint("accent", "/progress")}${chalk82.dim(" hours")}`,
36912
37099
  cardW
36913
37100
  )
36914
37101
  );
@@ -36945,6 +37132,7 @@ var init_welcome = __esm({
36945
37132
  init_verify();
36946
37133
  init_queries();
36947
37134
  init_schema();
37135
+ init_registry();
36948
37136
  NO_SUMMARY = "(no summary)";
36949
37137
  CARD_MAX_W = 128;
36950
37138
  CARD_SIDE_MARGIN = 6;
@@ -37168,10 +37356,10 @@ async function runRepl(ctx, version) {
37168
37356
  console.log();
37169
37357
  if (ctx.pendingUpdateCheck) {
37170
37358
  void ctx.pendingUpdateCheck.then((result) => {
37171
- if (result?.updateAvailable) {
37172
- console.log(` ${formatUpdateNudge(result.current, result.latest)}`);
37173
- console.log();
37174
- }
37359
+ if (!result?.updateAvailable) return;
37360
+ if (result.latest === ctx.updateHomePaintedLatest) return;
37361
+ console.log(` ${formatUpdateNudge(result.current, result.latest)}`);
37362
+ console.log();
37175
37363
  });
37176
37364
  }
37177
37365
  let pendingSuggestionRender = null;
@@ -37353,6 +37541,7 @@ function printHelp() {
37353
37541
  ["/end", "Close the session"],
37354
37542
  ["/recap", "Write a recap"],
37355
37543
  ["/connect", "Connect a key"],
37544
+ ["/activate", "Paste a license key"],
37356
37545
  ["/upgrade", "Change to Pro"],
37357
37546
  ["/checkout", "Open signup"],
37358
37547
  ["/update", "Install the latest version"],
@@ -37597,28 +37786,22 @@ async function main() {
37597
37786
  ctx.datasetPath = datasetPathForSession2(ctx.sessionId);
37598
37787
  await setActiveDbPath2(ctx.datasetPath);
37599
37788
  const { completeInteractiveSetup: completeInteractiveSetup2 } = await Promise.resolve().then(() => (init_first_run(), first_run_exports));
37600
- await completeInteractiveSetup2(ctx, { skipBrand: showedActivation });
37601
- const {
37602
- isCacheFresh: isCacheFresh2,
37603
- loadUpdateCheckCache: loadUpdateCheckCache2
37604
- } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
37605
37789
  const {
37606
- checkForUpdate: checkForUpdate2,
37607
- formatUpdateNudge: formatUpdateNudge2,
37608
- isNewerVersion: isNewerVersion2
37790
+ hydrateUpdateAvailableFromCache: hydrateUpdateAvailableFromCache2,
37791
+ startBackgroundUpdateCheck: startBackgroundUpdateCheck2
37609
37792
  } = await Promise.resolve().then(() => (init_registry(), registry_exports));
37610
- const currentVersion = getInstalledVersion();
37611
- const cachedUpdate = loadUpdateCheckCache2();
37612
- if (isCacheFresh2(cachedUpdate)) {
37613
- if (cachedUpdate.latestVersion && isNewerVersion2(cachedUpdate.latestVersion, currentVersion)) {
37614
- console.log(formatUpdateNudge2(currentVersion, cachedUpdate.latestVersion));
37615
- }
37616
- } else {
37617
- ctx.pendingUpdateCheck = checkForUpdate2({ timeoutMs: 200 });
37618
- }
37793
+ const { consumeJustUpdatedEnv: consumeJustUpdatedEnv2 } = await Promise.resolve().then(() => (init_relaunch(), relaunch_exports));
37794
+ ctx.updateAvailable = hydrateUpdateAvailableFromCache2(VERSION);
37795
+ startBackgroundUpdateCheck2(ctx);
37796
+ await completeInteractiveSetup2(ctx, { skipBrand: showedActivation });
37619
37797
  const { startSessionTranscript: startSessionTranscript2, stopSessionTranscript: stopSessionTranscript2 } = await Promise.resolve().then(() => (init_transcript(), transcript_exports));
37620
37798
  startSessionTranscript2(ctx);
37621
37799
  const { printWelcome: printWelcome2 } = await Promise.resolve().then(() => (init_welcome(), welcome_exports));
37800
+ const justUpdated = consumeJustUpdatedEnv2();
37801
+ if (justUpdated) {
37802
+ console.log();
37803
+ console.log(" " + paint("accent", "\u2713") + " " + chalk84.dim(`Now running v${justUpdated.to}`));
37804
+ }
37622
37805
  await printWelcome2(ctx, VERSION);
37623
37806
  await runRepl(ctx, VERSION);
37624
37807
  stopSessionTranscript2();