@sonnechasser/ntrp 1.3.5 → 1.3.6

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.
@@ -527,6 +527,16 @@ var init_terminal_capture = __esm({
527
527
  // src/output/redact-write.ts
528
528
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
529
529
  import { dirname } from "path";
530
+ function redactExportText(content) {
531
+ return redactSecrets(content);
532
+ }
533
+ function writeRedactedText(path, content) {
534
+ mkdirSync2(dirname(path), { recursive: true });
535
+ writeFileSync2(path, redactExportText(content), "utf-8");
536
+ }
537
+ function isRedactableExportPath(path) {
538
+ return /\.(md|markdown|txt)$/i.test(path);
539
+ }
530
540
  var init_redact_write = __esm({
531
541
  "src/output/redact-write.ts"() {
532
542
  "use strict";
@@ -537,6 +547,17 @@ var init_redact_write = __esm({
537
547
  // src/output/path-safety.ts
538
548
  import { homedir as homedir2 } from "os";
539
549
  import { resolve as resolve2, sep } from "path";
550
+ function resolveUserPath(path) {
551
+ if (path === "~" || path.startsWith("~/") || path.startsWith("~\\")) {
552
+ return resolve2(homedir2(), path.slice(2));
553
+ }
554
+ return resolve2(path);
555
+ }
556
+ function isInsideNtrp(path) {
557
+ const home = ntrpHome();
558
+ const resolved = resolve2(path);
559
+ return resolved === home || resolved.startsWith(home + sep);
560
+ }
540
561
  var NTRP_HOME;
541
562
  var init_path_safety = __esm({
542
563
  "src/output/path-safety.ts"() {
@@ -547,6 +568,28 @@ var init_path_safety = __esm({
547
568
  });
548
569
 
549
570
  // src/services/export-kinds.ts
571
+ function latestBasenameForKind(kind) {
572
+ if (kind.startsWith("prompt:")) {
573
+ const target = kind.slice("prompt:".length);
574
+ return target ? `handoff-${target}.md` : "handoff.md";
575
+ }
576
+ if (kind === "report") return "report.md";
577
+ if (kind === "notes") return "notes.md";
578
+ if (kind === "csv") return "csv";
579
+ if (kind === "publish") return "publish";
580
+ return "handoff.md";
581
+ }
582
+ function inboxLatestNameForKind(kind) {
583
+ if (kind.startsWith("prompt:")) {
584
+ const target = kind.slice("prompt:".length);
585
+ return target ? `latest-handoff-${target}.md` : "latest-handoff.md";
586
+ }
587
+ if (kind === "report") return "latest-report.md";
588
+ if (kind === "notes") return "latest-notes.md";
589
+ if (kind === "csv") return "latest-csv";
590
+ if (kind === "publish") return "latest-publish";
591
+ return "latest-handoff.md";
592
+ }
550
593
  var init_export_kinds = __esm({
551
594
  "src/services/export-kinds.ts"() {
552
595
  "use strict";
@@ -646,6 +689,243 @@ import { mkdirSync as mkdirSync3 } from "fs";
646
689
  import { basename, join as join2 } from "path";
647
690
  import { homedir as homedir3 } from "os";
648
691
  import chalk2 from "chalk";
692
+ function defaultAiInboxDir() {
693
+ return join2(homedir3(), "Documents", "Claude", "ntrp-inbox");
694
+ }
695
+ function handoffLocations() {
696
+ const archiveRoot = getExportsDir();
697
+ const archiveLatestDir = join2(archiveRoot, "latest");
698
+ const inboxDir = getConfiguredAiInboxDir();
699
+ return {
700
+ archiveRoot,
701
+ archiveIndex: join2(archiveRoot, "INDEX.md"),
702
+ archiveLatestDir,
703
+ archiveSkill: join2(archiveLatestDir, STANDING_SKILL_NAME),
704
+ archivePickup: join2(archiveLatestDir, ARCHIVE_PICKUP_NAME),
705
+ inboxDir,
706
+ inboxIndex: inboxDir ? join2(inboxDir, "INDEX.md") : null,
707
+ inboxSkill: inboxDir ? join2(inboxDir, STANDING_SKILL_NAME) : null,
708
+ inboxPickup: inboxDir ? join2(inboxDir, INBOX_PICKUP_NAME) : null
709
+ };
710
+ }
711
+ function pickupContextFromEvent(event) {
712
+ const loc = handoffLocations();
713
+ const writtenAt = event.at || (/* @__PURE__ */ new Date()).toISOString();
714
+ const dateUtc = writtenAt.slice(0, 10);
715
+ const timeUtc = writtenAt.slice(11, 16);
716
+ const inboxLatest = loc.inboxDir ? join2(loc.inboxDir, inboxLatestNameForKind(event.kind)) : null;
717
+ const inboxGenericLatest = loc.inboxDir && event.kind.startsWith("prompt:") ? join2(loc.inboxDir, "latest-handoff.md") : inboxLatest;
718
+ return {
719
+ ...loc,
720
+ kind: event.kind,
721
+ title: event.title,
722
+ writtenAt,
723
+ archivePath: event.path,
724
+ archiveLatest: join2(loc.archiveLatestDir, latestBasenameForKind(event.kind)),
725
+ inboxLatest,
726
+ inboxGenericLatest,
727
+ datedBasename: basename(event.path),
728
+ dateUtc,
729
+ timeUtc
730
+ };
731
+ }
732
+ function kindLabel(kind) {
733
+ if (kind.startsWith("prompt:")) {
734
+ const target = kind.slice("prompt:".length);
735
+ if (target === "deck") return "deck prompt";
736
+ if (target === "plan") return "action-plan prompt";
737
+ if (target === "asana") return "Asana project prompt";
738
+ if (target === "clay") return "Clay table prompt";
739
+ return target ? `${target} prompt` : "agent prompt";
740
+ }
741
+ if (kind === "report") return "markdown report";
742
+ if (kind === "notes") return "notes export";
743
+ if (kind === "csv") return "CSV receipts";
744
+ if (kind === "publish") return "repository export";
745
+ return kind;
746
+ }
747
+ function jobForKind(kind) {
748
+ if (kind === "prompt:deck") {
749
+ return "Open that file and follow its instructions to build an executive review deck. Do not invent numbers.";
750
+ }
751
+ if (kind === "prompt:plan") {
752
+ return "Open that file and follow its instructions to build a prioritized action plan. Do not invent numbers.";
753
+ }
754
+ if (kind === "prompt:asana") {
755
+ return "Open that file and follow its instructions to create the Asana project (sections + tasks). Do not invent numbers.";
756
+ }
757
+ if (kind === "prompt:clay") {
758
+ return "Open that file and follow its instructions to spec the Clay table. Do not invent numbers.";
759
+ }
760
+ if (kind.startsWith("prompt:")) {
761
+ return "Open that file and follow its instructions to build the deliverable. Do not invent numbers.";
762
+ }
763
+ if (kind === "report") {
764
+ return "Open the markdown report. Brief or restyle it as asked; do not invent numbers.";
765
+ }
766
+ if (kind === "notes") {
767
+ return "Open the notes file (Obsidian-style GTM write-up). Use it as source material; do not invent numbers.";
768
+ }
769
+ if (kind === "csv") {
770
+ return "Open the CSV receipts folder (cover-sheet.csv plus per-vital evidence). Use those files as source data; do not invent numbers.";
771
+ }
772
+ if (kind === "publish") {
773
+ return "Open the repository export package folder and work from the files inside.";
774
+ }
775
+ return "Open the file and use it as source material. Do not invent numbers.";
776
+ }
777
+ function filenamePattern(ctx) {
778
+ const base = ctx.datedBasename;
779
+ const dot = base.lastIndexOf(".");
780
+ if (dot <= 0) return `${base}*`;
781
+ const stem = base.slice(0, dot);
782
+ const ext = base.slice(dot);
783
+ const datePrefix = stem.includes(ctx.dateUtc) ? `${stem.split(ctx.dateUtc)[0]}${ctx.dateUtc}` : stem.slice(0, 12);
784
+ return `${datePrefix}*${ext}`;
785
+ }
786
+ function buildStandingSkillMarkdown(loc = handoffLocations()) {
787
+ const inboxBlock = loc.inboxDir ? `Inbox (preferred \u2014 point Claude Desktop / a project / Cursor at this folder):
788
+ \`${loc.inboxDir}\`
789
+
790
+ Start with:
791
+ - \`SKILL.md\` \u2014 this file
792
+ - \`latest-pickup.md\` \u2014 the handoff that was just written (date + exact paths)
793
+ - \`latest-handoff.md\` / \`latest-handoff-<target>.md\` \u2014 newest agent prompt
794
+ - \`latest-report.md\`, \`latest-notes.md\`, \`latest-csv\` \u2014 other kinds
795
+ - \`INDEX.md\` \u2014 catalog with timestamps
796
+ - \`archive/\` \u2014 dated copies` : `No AI inbox is configured yet. Canonical archive (always written):
797
+ \`${loc.archiveRoot}\`
798
+
799
+ Ask the operator to run \`/inbox set <folder>\` in ntrp so copies land in a folder you can see. Until then, use the archive paths below.`;
800
+ return `---
801
+ name: ntrp-handoff
802
+ description: Find and execute NTRP GTM analysis handoffs (deck, plan, Asana, Clay, report, notes, CSV) from the local inbox or exports archive. Use when the user mentions an NTRP handoff, board deck, action plan, or a file ntrp just wrote.
803
+ ---
804
+
805
+ # Find an NTRP handoff
806
+
807
+ NTRP writes GTM analysis deliverables to disk. Your job is to open the file and follow it \u2014 do not invent numbers.
808
+
809
+ ## Where to look (this machine)
810
+
811
+ ${inboxBlock}
812
+
813
+ Canonical archive:
814
+ \`${loc.archiveRoot}\`
815
+
816
+ - \`latest/SKILL.md\` \u2014 this finder
817
+ - \`latest/pickup.md\` \u2014 the handoff that was just written
818
+ - \`latest/handoff.md\` / \`latest/handoff-<target>.md\` \u2014 newest prompt
819
+ - \`INDEX.md\` \u2014 catalog with timestamps and move history
820
+ - \`handoffs/\`, \`reports/\`, \`notes/\`, \`csv/\`, \`publish/\` \u2014 dated files by kind
821
+
822
+ ## How to pick the file
823
+
824
+ 1. If they just ran a handoff, open \`latest-pickup.md\` (inbox) or \`latest/pickup.md\` (archive). It names the exact file and date.
825
+ 2. Otherwise prefer the stable pointer for what they asked for:
826
+ - deck / slides \u2192 \`latest-handoff-deck.md\` (inbox) or \`latest/handoff-deck.md\` (archive)
827
+ - action plan \u2192 \`latest-handoff-plan.md\`
828
+ - Asana \u2192 \`latest-handoff-asana.md\`
829
+ - Clay \u2192 \`latest-handoff-clay.md\`
830
+ - any prompt \u2192 \`latest-handoff.md\` / \`latest/handoff.md\`
831
+ - report / notes / CSV \u2192 \`latest-report.md\`, \`latest-notes.md\`, \`latest-csv\`
832
+ 3. If they mention a date, open \`INDEX.md\` and pick the newest row on that UTC date. Dated filenames look like \`handoff-deck-2026-08-13-150123.md\`.
833
+ 4. If none of those paths are in your workspace, ask them to attach the file or to \`/inbox set\` a folder you can read.
834
+
835
+ You are not given a fresh path on every handoff. Prefer the stable \`latest-*\` pointers.
836
+
837
+ Then execute the instructions in that file.
838
+ `;
839
+ }
840
+ function buildPickupPrompt(ctx) {
841
+ const lines = [
842
+ `Find the NTRP GTM handoff written ${ctx.dateUtc} at ${ctx.timeUtc} UTC.`,
843
+ `Kind: ${kindLabel(ctx.kind)}${ctx.title ? ` (${ctx.title})` : ""}.`,
844
+ "",
845
+ jobForKind(ctx.kind),
846
+ "",
847
+ "Look in this order (this machine):",
848
+ ""
849
+ ];
850
+ let n = 1;
851
+ if (ctx.inboxLatest) {
852
+ lines.push(`${n}. Inbox pointer: ${ctx.inboxLatest}`);
853
+ n++;
854
+ }
855
+ if (ctx.inboxGenericLatest && ctx.inboxGenericLatest !== ctx.inboxLatest) {
856
+ lines.push(`${n}. Inbox generic: ${ctx.inboxGenericLatest}`);
857
+ n++;
858
+ }
859
+ lines.push(`${n}. Archive pointer: ${ctx.archiveLatest}`);
860
+ n++;
861
+ lines.push(`${n}. Dated file: ${ctx.archivePath}`);
862
+ lines.push(
863
+ "",
864
+ "If those paths are not in your workspace:",
865
+ `- Open INDEX.md in ${ctx.inboxDir ?? ctx.archiveRoot}`,
866
+ `- Pick the newest row dated ${ctx.dateUtc} matching ${ctx.kind}`,
867
+ `- Or search for ${filenamePattern(ctx)}`,
868
+ "",
869
+ "Standing finder skill (same folders, install once):",
870
+ `- ${ctx.inboxSkill ?? ctx.archiveSkill}`
871
+ );
872
+ if (ctx.inboxSkill) {
873
+ lines.push(`- ${ctx.archiveSkill}`);
874
+ } else {
875
+ lines.push("- No AI inbox yet \u2014 in ntrp run `/inbox set <folder>` so copies land where your agent can see them.");
876
+ }
877
+ return lines.join("\n") + "\n";
878
+ }
879
+ function persistStandingSkill(loc = handoffLocations()) {
880
+ mkdirSync3(loc.archiveLatestDir, { recursive: true });
881
+ const md = buildStandingSkillMarkdown(loc);
882
+ writeRedactedText(loc.archiveSkill, md);
883
+ if (loc.inboxDir && loc.inboxSkill) {
884
+ mkdirSync3(loc.inboxDir, { recursive: true });
885
+ writeRedactedText(loc.inboxSkill, md);
886
+ }
887
+ }
888
+ function persistHandoffSkillFiles(event) {
889
+ const ctx = pickupContextFromEvent(event);
890
+ persistStandingSkill(ctx);
891
+ writeRedactedText(ctx.archivePickup, buildPickupPrompt(ctx));
892
+ if (ctx.inboxDir && ctx.inboxPickup) {
893
+ mkdirSync3(ctx.inboxDir, { recursive: true });
894
+ writeRedactedText(ctx.inboxPickup, buildPickupPrompt(ctx));
895
+ }
896
+ }
897
+ function printPlainBlock(text) {
898
+ const rule = " " + chalk2.dim("\u2500".repeat(60));
899
+ console.log(rule);
900
+ for (const line of text.replace(/\n$/, "").split("\n")) {
901
+ console.log(line.length > 0 ? ` ${line}` : " ");
902
+ }
903
+ console.log(rule);
904
+ }
905
+ function maybePrintAiInboxNudge() {
906
+ if (getConfiguredAiInboxDir()) return;
907
+ if (getConfigValue("ai-inbox-nudge-seen") === "true") return;
908
+ setConfigValue("ai-inbox-nudge-seen", "true");
909
+ console.log(
910
+ " " + chalk2.dim("Set a pickup folder. Type /inbox set ~/Documents/Claude/ntrp-inbox then /inbox skill")
911
+ );
912
+ }
913
+ function printStandingSkill() {
914
+ persistStandingSkill();
915
+ const loc = handoffLocations();
916
+ console.log();
917
+ console.log(" " + bold("NTRP handoff finder skill"));
918
+ console.log(
919
+ " " + chalk2.dim("Paste this skill once into Claude, ChatGPT, or Cursor. Later handoffs do not print it again.")
920
+ );
921
+ printPlainBlock(buildStandingSkillMarkdown(loc));
922
+ console.log(" " + chalk2.dim("Written: ") + (loc.inboxSkill ?? loc.archiveSkill));
923
+ if (!loc.inboxDir) {
924
+ maybePrintAiInboxNudge();
925
+ }
926
+ console.log();
927
+ }
928
+ var STANDING_SKILL_NAME, ARCHIVE_PICKUP_NAME, INBOX_PICKUP_NAME;
649
929
  var init_handoff_skill = __esm({
650
930
  "src/services/handoff-skill.ts"() {
651
931
  "use strict";
@@ -653,6 +933,9 @@ var init_handoff_skill = __esm({
653
933
  init_store();
654
934
  init_theme();
655
935
  init_export_kinds();
936
+ STANDING_SKILL_NAME = "SKILL.md";
937
+ ARCHIVE_PICKUP_NAME = "pickup.md";
938
+ INBOX_PICKUP_NAME = "latest-pickup.md";
656
939
  }
657
940
  });
658
941
 
@@ -693,10 +976,262 @@ function ensureExportsLayout(root = getExportsDir()) {
693
976
  function getAiInboxDir() {
694
977
  return getConfiguredAiInboxDir();
695
978
  }
979
+ function setAiInboxDir(path) {
980
+ const resolved = resolveUserPath(path);
981
+ mkdirSync4(resolved, { recursive: true });
982
+ setConfigValue("ai-inbox-dir", resolved);
983
+ ensureInboxLayout(resolved);
984
+ persistStandingSkill();
985
+ setConfigValue("ai-inbox-nudge-seen", "true");
986
+ return resolved;
987
+ }
988
+ function ensureInboxLayout(inbox) {
989
+ mkdirSync4(inbox, { recursive: true });
990
+ mkdirSync4(join3(inbox, "archive"), { recursive: true });
991
+ const readme = join3(inbox, "README.md");
992
+ writeFileSync3(readme, buildInboxReadme(), "utf-8");
993
+ if (!existsSync2(join3(inbox, "INDEX.md"))) {
994
+ writeFileSync3(join3(inbox, "INDEX.md"), "# NTRP AI inbox\n\n_No exports synced yet._\n", "utf-8");
995
+ }
996
+ }
997
+ function manifestPath(root = getExportsDir()) {
998
+ return join3(root, "manifest.jsonl");
999
+ }
1000
+ function readManifestEvents(root = getExportsDir()) {
1001
+ const path = manifestPath(root);
1002
+ if (!existsSync2(path)) return [];
1003
+ const text = readFileSync2(path, "utf-8");
1004
+ const events = [];
1005
+ for (const line of text.split("\n")) {
1006
+ const trimmed = line.trim();
1007
+ if (!trimmed) continue;
1008
+ try {
1009
+ events.push(JSON.parse(trimmed));
1010
+ } catch {
1011
+ }
1012
+ }
1013
+ return events;
1014
+ }
1015
+ function appendManifestEvent(event, root = getExportsDir()) {
1016
+ ensureExportsLayout(root);
1017
+ appendFileSync(manifestPath(root), JSON.stringify(event) + "\n", "utf-8");
1018
+ }
1019
+ function listExports(opts = {}) {
1020
+ const limit = opts.limit ?? 20;
1021
+ const events = readManifestEvents();
1022
+ const byId2 = /* @__PURE__ */ new Map();
1023
+ for (const e of events) {
1024
+ if (e.op === "inbox_sync") continue;
1025
+ byId2.set(e.id, e);
1026
+ }
1027
+ let items = [...byId2.values()].sort((a, b) => a.at < b.at ? 1 : a.at > b.at ? -1 : 0);
1028
+ if (opts.kind) {
1029
+ const k = opts.kind.toLowerCase();
1030
+ items = items.filter((e) => e.kind === opts.kind || e.kind.startsWith(k) || e.kind.includes(k));
1031
+ }
1032
+ return items.slice(0, limit);
1033
+ }
1034
+ function copyPath(src, dest) {
1035
+ mkdirSync4(dirname2(dest), { recursive: true });
1036
+ if (existsSync2(dest)) {
1037
+ rmSync(dest, { recursive: true, force: true });
1038
+ }
1039
+ const st = statSync(src);
1040
+ if (st.isDirectory()) {
1041
+ cpSync(src, dest, { recursive: true });
1042
+ } else if (isRedactableExportPath(src)) {
1043
+ const text = readFileSync2(src, "utf-8");
1044
+ writeFileSync3(dest, redactExportText(text), "utf-8");
1045
+ } else {
1046
+ copyFileSync(src, dest);
1047
+ }
1048
+ }
1049
+ function regenerateIndex(root = getExportsDir()) {
1050
+ ensureExportsLayout(root);
1051
+ const items = listExports({ limit: 50 });
1052
+ const events = readManifestEvents(root);
1053
+ const withHistory = items.filter((e) => (e.previous_paths?.length ?? 0) > 0);
1054
+ const latestDir = join3(root, "latest");
1055
+ const latestLines = [];
1056
+ if (existsSync2(latestDir)) {
1057
+ for (const name of readdirSync(latestDir).sort()) {
1058
+ latestLines.push(`- \`latest/${name}\` \u2192 \`${join3(latestDir, name)}\``);
1059
+ }
1060
+ }
1061
+ const lines = [
1062
+ "# NTRP exports",
1063
+ "",
1064
+ `Archive root: \`${root}\``,
1065
+ "",
1066
+ "Set an inbox with `/onboard` or `/inbox set`. Paste `SKILL.md` once into Claude. Later handoffs overwrite `latest-handoff.md`.",
1067
+ "",
1068
+ "## Latest pointers",
1069
+ ""
1070
+ ];
1071
+ if (latestLines.length > 0) lines.push(...latestLines);
1072
+ else lines.push("_None yet._");
1073
+ lines.push("", "## Recent exports", "");
1074
+ if (items.length === 0) {
1075
+ lines.push("_No exports yet._");
1076
+ } else {
1077
+ for (const e of items) {
1078
+ const title = e.title ? ` \u2014 ${e.title}` : "";
1079
+ const session = e.session_id ? ` \xB7 session ${e.session_id.slice(-4)}` : "";
1080
+ lines.push(`- **${e.kind}** (${e.at})${title}${session}`);
1081
+ lines.push(` - id: \`${e.id}\``);
1082
+ lines.push(` - path: \`${e.path}\``);
1083
+ if (e.inbox_path) lines.push(` - inbox: \`${e.inbox_path}\``);
1084
+ }
1085
+ }
1086
+ lines.push("", "## Location history", "");
1087
+ if (withHistory.length === 0) {
1088
+ lines.push("_No moves recorded._");
1089
+ } else {
1090
+ for (const e of withHistory) {
1091
+ lines.push(`- **${e.kind}** \`${e.id}\``);
1092
+ for (const prev of e.previous_paths ?? []) {
1093
+ lines.push(` - was: \`${prev}\``);
1094
+ }
1095
+ lines.push(` - now: \`${e.path}\``);
1096
+ }
1097
+ }
1098
+ const moveOps = events.filter((e) => e.op === "move").slice(-20).reverse();
1099
+ if (moveOps.length > 0) {
1100
+ lines.push("", "## Recent moves", "");
1101
+ for (const e of moveOps) {
1102
+ const from = e.previous_paths?.[e.previous_paths.length - 1] ?? "?";
1103
+ lines.push(`- ${e.at}: \`${from}\` \u2192 \`${e.path}\` (${e.kind}, \`${e.id}\`)`);
1104
+ }
1105
+ }
1106
+ lines.push("");
1107
+ writeFileSync3(join3(root, "INDEX.md"), lines.join("\n"), "utf-8");
1108
+ }
1109
+ function regenerateInboxIndex(inbox) {
1110
+ ensureInboxLayout(inbox);
1111
+ const items = listExports({ limit: 15 });
1112
+ const archiveRoot = getExportsDir();
1113
+ const lines = [
1114
+ "# NTRP AI inbox",
1115
+ "",
1116
+ "Start here. Install `SKILL.md` once in Claude. Newest prompt: `latest-handoff.md`. Catalog: `INDEX.md`.",
1117
+ "",
1118
+ `Canonical archive: \`${archiveRoot}\` (see \`${join3(archiveRoot, "INDEX.md")}\`).`,
1119
+ "",
1120
+ "## Latest pointers",
1121
+ ""
1122
+ ];
1123
+ if (existsSync2(join3(inbox, "SKILL.md"))) {
1124
+ lines.push("- [`SKILL.md`](./SKILL.md) \u2014 standing finder. Paste once into your agent.");
1125
+ }
1126
+ const latestNames = readdirSync(inbox).filter((n) => n.startsWith("latest-")).sort();
1127
+ if (latestNames.length === 0 && !existsSync2(join3(inbox, "SKILL.md"))) {
1128
+ lines.push("_None yet. Run a handoff after `/inbox set`._");
1129
+ } else {
1130
+ for (const name of latestNames) {
1131
+ const note = name === "latest-pickup.md" ? " \u2014 names the file that was just written" : "";
1132
+ lines.push(`- [\`${name}\`](./${name})${note}`);
1133
+ }
1134
+ }
1135
+ lines.push("", "## Recent exports", "");
1136
+ if (items.length === 0) lines.push("_No exports yet._");
1137
+ else {
1138
+ for (const e of items) {
1139
+ lines.push(`- **${e.kind}** (${e.at}): \`${e.path}\``);
1140
+ if (e.inbox_path) lines.push(` - inbox copy: \`${e.inbox_path}\``);
1141
+ }
1142
+ }
1143
+ lines.push("");
1144
+ writeFileSync3(join3(inbox, "INDEX.md"), lines.join("\n"), "utf-8");
1145
+ writeFileSync3(join3(inbox, "README.md"), buildInboxReadme(), "utf-8");
1146
+ }
1147
+ function buildInboxReadme() {
1148
+ const archive = getExportsDir();
1149
+ return `# NTRP AI inbox
1150
+
1151
+ This folder is the landing folder for NTRP handoffs. Desktop AI tools read files here.
1152
+
1153
+ ## Start here
1154
+
1155
+ 1. Install \`SKILL.md\` once in Claude, ChatGPT, or Cursor. Then tell the agent to open the latest NTRP handoff.
1156
+ 2. The newest prompt is \`latest-handoff.md\` (or \`latest-handoff-deck.md\` and similar).
1157
+ 3. \`latest-pickup.md\` names the file that was just written.
1158
+
1159
+ Each export overwrites the \`latest-*\` files. Dated copies are in \`archive/\`.
1160
+
1161
+ ## Canonical archive
1162
+
1163
+ The full history with the move trail is at:
1164
+
1165
+ \`${archive}\`
1166
+
1167
+ See \`${join3(archive, "INDEX.md")}\` and \`${join3(archive, "manifest.jsonl")}\`.
1168
+
1169
+ Set the folder with \`/inbox set <path>\`. Print the skill with \`/inbox skill\`. Clear with \`/inbox clear\`. List files with \`/exports\`.
1170
+ `;
1171
+ }
1172
+ function pruneInboxArchive(archiveDir, keep = INBOX_ARCHIVE_KEEP) {
1173
+ if (!existsSync2(archiveDir)) return;
1174
+ const entries2 = readdirSync(archiveDir).map((name) => {
1175
+ const p = join3(archiveDir, name);
1176
+ try {
1177
+ return { name, path: p, mtime: statSync(p).mtimeMs };
1178
+ } catch {
1179
+ return null;
1180
+ }
1181
+ }).filter((e) => e != null).sort((a, b) => b.mtime - a.mtime);
1182
+ for (const old of entries2.slice(keep)) {
1183
+ rmSync(old.path, { recursive: true, force: true });
1184
+ }
1185
+ }
1186
+ function syncAiInbox(entry) {
1187
+ const inbox = getAiInboxDir();
1188
+ if (!inbox) return null;
1189
+ if (!existsSync2(entry.path)) return null;
1190
+ ensureInboxLayout(inbox);
1191
+ const archiveDir = join3(inbox, "archive");
1192
+ mkdirSync4(archiveDir, { recursive: true });
1193
+ const base = basename2(entry.path);
1194
+ const archiveDest = join3(archiveDir, base);
1195
+ copyPath(entry.path, archiveDest);
1196
+ pruneInboxArchive(archiveDir);
1197
+ const latestName = inboxLatestNameForKind(entry.kind);
1198
+ const latestDest = join3(inbox, latestName);
1199
+ copyPath(entry.path, latestDest);
1200
+ if (entry.kind.startsWith("prompt:")) {
1201
+ copyPath(entry.path, join3(inbox, "latest-handoff.md"));
1202
+ }
1203
+ regenerateInboxIndex(inbox);
1204
+ return latestDest;
1205
+ }
1206
+ function syncRecentToInbox(limit = 10) {
1207
+ const inbox = getAiInboxDir();
1208
+ if (!inbox) return 0;
1209
+ ensureInboxLayout(inbox);
1210
+ const items = listExports({ limit });
1211
+ let n = 0;
1212
+ for (const item of items) {
1213
+ if (!existsSync2(item.path)) continue;
1214
+ const inboxPath = syncAiInbox(item);
1215
+ if (inboxPath) {
1216
+ appendManifestEvent({
1217
+ ...item,
1218
+ op: "inbox_sync",
1219
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1220
+ inbox_path: inboxPath
1221
+ });
1222
+ n++;
1223
+ }
1224
+ }
1225
+ regenerateInboxIndex(inbox);
1226
+ regenerateIndex();
1227
+ const newest = items.find((e) => existsSync2(e.path));
1228
+ if (newest) persistHandoffSkillFiles(newest);
1229
+ return n;
1230
+ }
696
1231
  function archiveIndexPath() {
697
1232
  return join3(ensureExportsLayout(), "INDEX.md");
698
1233
  }
699
- var KIND_DIRS, ARCHIVE_README;
1234
+ var KIND_DIRS, INBOX_ARCHIVE_KEEP, ARCHIVE_README;
700
1235
  var init_exports_registry = __esm({
701
1236
  "src/services/exports-registry.ts"() {
702
1237
  "use strict";
@@ -707,6 +1242,7 @@ var init_exports_registry = __esm({
707
1242
  init_handoff_skill();
708
1243
  init_export_kinds();
709
1244
  KIND_DIRS = ["handoffs", "reports", "notes", "csv", "publish"];
1245
+ INBOX_ARCHIVE_KEEP = 20;
710
1246
  ARCHIVE_README = `# NTRP exports archive
711
1247
 
712
1248
  This archive stores handoffs, reports, notes, CSV receipts, and publish packages by kind:
@@ -7999,7 +8535,6 @@ function resetContextForSwitch(ctx, opts) {
7999
8535
  ctx.deliverIntent = false;
8000
8536
  ctx.computeInProgress = false;
8001
8537
  ctx.wizardDepth = 0;
8002
- ctx.welcomeLogoShown = false;
8003
8538
  ctx.snapshot = { computeResult: null, divergences: [] };
8004
8539
  rebindSessionTranscript(ctx);
8005
8540
  }
@@ -9289,8 +9824,8 @@ handler: ../commands/handoff.ts
9289
9824
  Write the analysis as an output. Choose a markdown report, notes, CSV receipts, or a repository package.
9290
9825
  Or write a prompt. Another agent can turn that prompt into a review deck, Asana project, Clay table, or action plan.
9291
9826
  Files go to \`export-dir\`. If set, files also go to the inbox.
9292
- Type \`/inbox set\` to set the inbox. \`/onboard\` also offers this step.
9293
- Type \`/inbox skill\` once. Or paste the skill during onboard.
9827
+ Type \`/inbox set\` to set the inbox. Demo setup offers this once. The first time you load your own data, NTRP asks again if the folder is not set. Skip then, and NTRP will not ask again \u2014 type \`/inbox set\` later.
9828
+ Type \`/inbox skill\` once. Or paste the skill during demo or that first production step.
9294
9829
  Later \`/handoff\` writes to \`latest-handoff.md\` with no new paste.
9295
9830
  Type \`/handoff prompt <target> --print\` to include the file body.
9296
9831
  An output marks the session as delivered.`
@@ -9323,7 +9858,7 @@ Set a folder that Claude Desktop or any desktop AI can read.
9323
9858
  NTRP copies each handoff to that folder.
9324
9859
  NTRP overwrites \`latest-handoff.md\` and \`latest-handoff-deck.md\`. The app then finds the newest file.
9325
9860
  NTRP also writes \`SKILL.md\`. That file holds finder instructions with your paths.
9326
- \`/onboard\` offers this step once. Type \`/inbox skill\` or \`/handoff skill\` to print it again.
9861
+ Demo setup offers this once. Loading your own data (CSV ingest or \`/onboard\`) asks again if the folder is not set. Skip on that production step and NTRP will not ask again. Type \`/inbox skill\` or \`/handoff skill\` to print the finder. Type \`/inbox set\` at any time.
9327
9862
  Paste that skill once into Claude, ChatGPT, or Cursor. Later handoffs need no new paste.
9328
9863
  \`INDEX.md\` in that folder links to the archive.
9329
9864
  A path outside ~/.ntrp needs a confirm. Handoffs carry analysis text.
@@ -9341,7 +9876,7 @@ handler: ../commands/onboard.ts
9341
9876
  Start the first-run wizard. It builds a company profile.
9342
9877
  Connect one or two keys (Anthropic, OpenAI, or another provider).
9343
9878
  Then answer a few seed questions. AI drafts industry, ICP, deal size, and stack guesses.
9344
- Optional last step: pick a folder for desktop-AI handoffs. Paste a finder skill into Claude, ChatGPT, or Cursor once.
9879
+ Optional last step: pick a folder for desktop-AI handoffs if it is not already set. Skip, and type \`/inbox set\` later \u2014 NTRP will not ask again.
9345
9880
  The profile is stored at \`~/.ntrp/profile.json\`. It flows into findings, NL answers, and demo data.`
9346
9881
  },
9347
9882
  {
@@ -9922,7 +10457,7 @@ args: <license>
9922
10457
  handler: ../commands/activate.ts
9923
10458
  ---
9924
10459
 
9925
- Activate NTRP with your license key (format: NTRP-XXXX-XXXX-XXXX). Most commands need a valid license.`
10460
+ Activate NTRP with the key from your purchase email. Most commands need a valid license.`
9926
10461
  },
9927
10462
  {
9928
10463
  name: "upgrade",
@@ -22157,12 +22692,126 @@ var init_revenue_importer = __esm({
22157
22692
  }
22158
22693
  });
22159
22694
 
22695
+ // src/conversation/inbox-setup.ts
22696
+ var inbox_setup_exports = {};
22697
+ __export(inbox_setup_exports, {
22698
+ maybeOfferInboxOnProduction: () => maybeOfferInboxOnProduction,
22699
+ offerInboxSkillSetup: () => offerInboxSkillSetup,
22700
+ shouldOfferInboxSkillSetup: () => shouldOfferInboxSkillSetup
22701
+ });
22702
+ import chalk22 from "chalk";
22703
+ function markDemoOffered() {
22704
+ setConfigValue("ai-inbox-nudge-seen", "true");
22705
+ }
22706
+ function markProductionOffered() {
22707
+ setConfigValue(PRODUCTION_OFFERED_KEY, "true");
22708
+ setConfigValue("ai-inbox-nudge-seen", "true");
22709
+ }
22710
+ function hasProductionOffered() {
22711
+ return getConfigValue(PRODUCTION_OFFERED_KEY) === "true";
22712
+ }
22713
+ function shouldOfferInboxSkillSetup(beat) {
22714
+ if (getAiInboxDir()) return false;
22715
+ if (beat === "demo") return getConfigValue("ai-inbox-nudge-seen") !== "true";
22716
+ return !hasProductionOffered();
22717
+ }
22718
+ function printSkipHint(beat) {
22719
+ const setCmd = paint("accent", "/inbox set ~/Documents/Claude/ntrp-inbox");
22720
+ const skillCmd = paint("accent", "/inbox skill");
22721
+ if (beat === "demo") {
22722
+ console.log(
22723
+ " " + chalk22.dim("Skipped. NTRP will ask once when you load your own data. Or type ") + setCmd + chalk22.dim(" then ") + skillCmd
22724
+ );
22725
+ return;
22726
+ }
22727
+ console.log(
22728
+ " " + chalk22.dim("Skipped. NTRP will not ask again. Type ") + setCmd + chalk22.dim(" then ") + skillCmd + chalk22.dim(" at any time.")
22729
+ );
22730
+ }
22731
+ async function offerInboxSkillSetup(session, opts = {}) {
22732
+ const beat = opts.beat ?? "production";
22733
+ if (!shouldOfferInboxSkillSetup(beat)) return;
22734
+ console.log();
22735
+ console.log(" " + bold("Set the handoff inbox"));
22736
+ console.log(
22737
+ " " + chalk22.dim(
22738
+ "Optional. NTRP copies each handoff into one folder. Paste the skill once. Later /handoff only writes the file."
22739
+ )
22740
+ );
22741
+ if (beat === "production" && getConfigValue("ai-inbox-nudge-seen") === "true") {
22742
+ console.log(" " + chalk22.dim("You skipped this during demo."));
22743
+ }
22744
+ console.log();
22745
+ const want = await session.confirm("Set a pickup folder for Claude, ChatGPT, or Cursor?", true);
22746
+ if (!want) {
22747
+ if (beat === "demo") markDemoOffered();
22748
+ else markProductionOffered();
22749
+ printSkipHint(beat);
22750
+ return;
22751
+ }
22752
+ const raw = await session.ask("Folder", { default: defaultAiInboxDir() });
22753
+ const folder = raw.trim() || defaultAiInboxDir();
22754
+ const resolvedPreview = resolveUserPath(folder);
22755
+ if (!isInsideNtrp(resolvedPreview)) {
22756
+ const ok = await session.confirm(
22757
+ `Inbox is outside ~/.ntrp (${resolvedPreview}). Handoffs with analysis text will be written here. Continue?`,
22758
+ true
22759
+ );
22760
+ if (!ok) {
22761
+ if (beat === "demo") markDemoOffered();
22762
+ else markProductionOffered();
22763
+ printSkipHint(beat);
22764
+ return;
22765
+ }
22766
+ }
22767
+ const resolved = setAiInboxDir(folder);
22768
+ const n = syncRecentToInbox(10);
22769
+ markDemoOffered();
22770
+ if (beat === "production") markProductionOffered();
22771
+ console.log();
22772
+ console.log(" " + paint("accent", "Inbox ready") + chalk22.dim(" ") + resolved);
22773
+ if (n > 0) {
22774
+ console.log(" " + chalk22.dim(`Synced ${n} recent export${n === 1 ? "" : "s"}.`));
22775
+ }
22776
+ console.log(
22777
+ " " + chalk22.dim("Paste this skill into Claude once. Later, tell Claude to open the latest NTRP handoff.")
22778
+ );
22779
+ printStandingSkill();
22780
+ await session.askPressEnter("Paste the skill into Claude. Then continue");
22781
+ console.log(" " + chalk22.dim("Done. Later handoffs write to that folder. The skill is not printed again."));
22782
+ console.log();
22783
+ }
22784
+ async function maybeOfferInboxOnProduction(ctx) {
22785
+ if (!shouldOfferInboxSkillSetup("production")) return;
22786
+ if (ctx.execution.mode === "headless" || ctx.execution.strictStdout) return;
22787
+ if (!ctx.rl && !process.stdin.isTTY) return;
22788
+ const session = createPromptSession(ctx.rl, ctx);
22789
+ try {
22790
+ await offerInboxSkillSetup(session, { beat: "production" });
22791
+ } finally {
22792
+ session.close();
22793
+ }
22794
+ }
22795
+ var PRODUCTION_OFFERED_KEY;
22796
+ var init_inbox_setup = __esm({
22797
+ "src/conversation/inbox-setup.ts"() {
22798
+ "use strict";
22799
+ init_prompts();
22800
+ init_store();
22801
+ init_theme();
22802
+ init_exports_registry();
22803
+ init_handoff_skill();
22804
+ init_path_safety();
22805
+ PRODUCTION_OFFERED_KEY = "ai-inbox-production-offered";
22806
+ }
22807
+ });
22808
+
22160
22809
  // src/commands/ingest.ts
22161
22810
  var ingest_exports = {};
22162
22811
  __export(ingest_exports, {
22163
22812
  handler: () => handler3
22164
22813
  });
22165
- import chalk22 from "chalk";
22814
+ import chalk23 from "chalk";
22166
22815
  import { readFileSync as readFileSync20, existsSync as existsSync22 } from "fs";
22167
22816
  import { basename as basename6 } from "path";
22168
22817
  async function handler3(args, ctx) {
@@ -22183,21 +22832,21 @@ async function handler3(args, ctx) {
22183
22832
  const source = getString(flags, "source", "s") ?? "salesforce";
22184
22833
  const skipResolve = getBool(flags, "skip-resolve");
22185
22834
  if (!file) {
22186
- console.error(chalk22.red(" Usage: /ingest <file> [--source salesforce|hubspot|outreach]"));
22187
- console.error(chalk22.dim(" /ingest --demo [--scenario <name>]"));
22835
+ console.error(chalk23.red(" Usage: /ingest <file> [--source salesforce|hubspot|outreach]"));
22836
+ console.error(chalk23.dim(" /ingest --demo [--scenario <name>]"));
22188
22837
  process.exit(1);
22189
22838
  }
22190
22839
  if (!existsSync22(file)) {
22191
- console.error(chalk22.red(` File not found: ${file}`));
22840
+ console.error(chalk23.red(` File not found: ${file}`));
22192
22841
  process.exit(1);
22193
22842
  }
22194
22843
  const profile = loadProfile();
22195
22844
  const skipProfile = getFalse(flags, "profile");
22196
22845
  if (!profile && !skipProfile) {
22197
22846
  console.error();
22198
- console.error(" " + chalk22.red("No company profile found."));
22199
- console.error(" " + chalk22.dim("Run ") + paint("accent", "/onboard") + chalk22.dim(" first for better column mapping,"));
22200
- console.error(" " + chalk22.dim("or pass ") + paint("accent", "--no-profile") + chalk22.dim(" to skip."));
22847
+ console.error(" " + chalk23.red("No company profile found."));
22848
+ console.error(" " + chalk23.dim("Run ") + paint("accent", "/onboard") + chalk23.dim(" first for better column mapping,"));
22849
+ console.error(" " + chalk23.dim("or pass ") + paint("accent", "--no-profile") + chalk23.dim(" to skip."));
22201
22850
  console.error();
22202
22851
  process.exit(1);
22203
22852
  }
@@ -22229,22 +22878,24 @@ async function handler3(args, ctx) {
22229
22878
  row_count: result2.imported
22230
22879
  });
22231
22880
  spinner.succeed(
22232
- `Imported ${chalk22.bold(result2.imported.toString())} revenue events from ${chalk22.dim(basename6(file))}`
22881
+ `Imported ${chalk23.bold(result2.imported.toString())} revenue events from ${chalk23.dim(basename6(file))}`
22233
22882
  );
22234
22883
  if (result2.errors.length > 0) {
22235
- console.log(chalk22.yellow(` ${result2.errors.length} rows skipped`));
22884
+ console.log(chalk23.yellow(` ${result2.errors.length} rows skipped`));
22236
22885
  }
22237
22886
  if (ctx.analysis) {
22238
22887
  ctx.analysis.data_source_type = "revenue_ledger";
22239
22888
  }
22240
- console.log(chalk22.dim(" Run ") + chalk22.cyan("/metrics") + chalk22.dim(" for SaaS metrics with ledger-backed retention."));
22889
+ console.log(chalk23.dim(" Run ") + chalk23.cyan("/metrics") + chalk23.dim(" for SaaS metrics with ledger-backed retention."));
22890
+ const { maybeOfferInboxOnProduction: maybeOfferInboxOnProduction3 } = await Promise.resolve().then(() => (init_inbox_setup(), inbox_setup_exports));
22891
+ await maybeOfferInboxOnProduction3(ctx);
22241
22892
  return `${result2.imported} revenue events from ${basename6(file)}`;
22242
22893
  }
22243
22894
  spinner.text = "Detecting entity type\u2026";
22244
22895
  const detection = detectEntityType(headers, source);
22245
22896
  if (!detection) {
22246
22897
  spinner.fail(`Could not auto-detect entity type for source: ${source}`);
22247
- console.log(chalk22.dim(" Headers found: " + headers.join(", ")));
22898
+ console.log(chalk23.dim(" Headers found: " + headers.join(", ")));
22248
22899
  process.exit(1);
22249
22900
  }
22250
22901
  spinner.text = `Importing ${rows.length} ${detection.entityType} rows...`;
@@ -22267,15 +22918,15 @@ async function handler3(args, ctx) {
22267
22918
  row_count: result.imported
22268
22919
  });
22269
22920
  spinner.succeed(
22270
- `Imported ${chalk22.bold(result.imported.toString())} ${detection.entityType} from ${chalk22.dim(basename6(file))} (${source})`
22921
+ `Imported ${chalk23.bold(result.imported.toString())} ${detection.entityType} from ${chalk23.dim(basename6(file))} (${source})`
22271
22922
  );
22272
22923
  if (result.errors.length > 0) {
22273
- console.log(chalk22.yellow(` ${result.errors.length} rows skipped`));
22924
+ console.log(chalk23.yellow(` ${result.errors.length} rows skipped`));
22274
22925
  for (const err of result.errors.slice(0, 3)) {
22275
- console.log(chalk22.dim(` - ${err}`));
22926
+ console.log(chalk23.dim(` - ${err}`));
22276
22927
  }
22277
22928
  if (result.errors.length > 3) {
22278
- console.log(chalk22.dim(` ... and ${result.errors.length - 3} more`));
22929
+ console.log(chalk23.dim(` ... and ${result.errors.length - 3} more`));
22279
22930
  }
22280
22931
  }
22281
22932
  if (!skipResolve) {
@@ -22289,10 +22940,12 @@ async function handler3(args, ctx) {
22289
22940
  resolveSpinner.succeed("No duplicates found");
22290
22941
  }
22291
22942
  }
22943
+ const { maybeOfferInboxOnProduction: maybeOfferInboxOnProduction2 } = await Promise.resolve().then(() => (init_inbox_setup(), inbox_setup_exports));
22944
+ await maybeOfferInboxOnProduction2(ctx);
22292
22945
  return `${result.imported} ${detection.entityType} from ${basename6(file)}`;
22293
22946
  } catch (err) {
22294
22947
  spinner.fail("Import failed");
22295
- console.error(chalk22.red(String(err)));
22948
+ console.error(chalk23.red(String(err)));
22296
22949
  process.exit(1);
22297
22950
  }
22298
22951
  }
@@ -22404,13 +23057,13 @@ __export(demo_fit_exports, {
22404
23057
  resolveDemoScenarioForLoad: () => resolveDemoScenarioForLoad,
22405
23058
  runDemoFitQuiz: () => runDemoFitQuiz
22406
23059
  });
22407
- import chalk23 from "chalk";
23060
+ import chalk24 from "chalk";
22408
23061
  async function runDemoFitQuiz(session, opts = {}) {
22409
23062
  if (opts.intro !== false) {
22410
23063
  console.log();
22411
23064
  console.log(" " + bold("Fit a sample book of business"));
22412
23065
  console.log(
22413
- " " + chalk23.dim(
23066
+ " " + chalk24.dim(
22414
23067
  "No API key is required. Answer two questions about how you sell. Then pick the closest of seven sample pipelines."
22415
23068
  )
22416
23069
  );
@@ -22428,8 +23081,8 @@ async function runDemoFitQuiz(session, opts = {}) {
22428
23081
  );
22429
23082
  const recommended = inferDemoScenario({ salesMotion: motion, dealBand });
22430
23083
  console.log();
22431
- console.log(" " + chalk23.dim("Recommended: ") + paint("accent", getScenario(recommended.scenario).label));
22432
- console.log(" " + chalk23.dim(recommended.reason));
23084
+ console.log(" " + chalk24.dim("Recommended: ") + paint("accent", getScenario(recommended.scenario).label));
23085
+ console.log(" " + chalk24.dim(recommended.reason));
22433
23086
  const scenario = await session.choose(
22434
23087
  "Which of these sample books feels closest to the one you manage?",
22435
23088
  scenarioMenuChoices(),
@@ -22449,8 +23102,8 @@ async function offerFittedDemoAfterOnboard(session, ctx, profile) {
22449
23102
  const s = getScenario(fit.scenario);
22450
23103
  console.log();
22451
23104
  console.log(" " + bold("A sample pipeline that looks like you"));
22452
- console.log(" " + paint("accent", s.label) + chalk23.dim(" \u2014 " + s.hook));
22453
- console.log(" " + chalk23.dim(fit.reason));
23105
+ console.log(" " + paint("accent", s.label) + chalk24.dim(" \u2014 " + s.hook));
23106
+ console.log(" " + chalk24.dim(fit.reason));
22454
23107
  console.log();
22455
23108
  const action = await session.choose(
22456
23109
  "Try NTRP on that book of business?",
@@ -22507,8 +23160,8 @@ async function resolveDemoScenarioForLoad(ctx, opts = {}) {
22507
23160
  if (!opts.forceQuiz && isProfileConfigured(profile) && profile) {
22508
23161
  const fit = await resolveProfileFit(profile, ctx);
22509
23162
  console.log();
22510
- console.log(" " + chalk23.dim("Recommended: ") + paint("accent", getScenario(fit.scenario).label));
22511
- console.log(" " + chalk23.dim(fit.reason));
23163
+ console.log(" " + chalk24.dim("Recommended: ") + paint("accent", getScenario(fit.scenario).label));
23164
+ console.log(" " + chalk24.dim(fit.reason));
22512
23165
  const scenario = await session.choose(
22513
23166
  "Which sample book of business?",
22514
23167
  scenarioMenuChoices(),
@@ -22572,7 +23225,7 @@ __export(ingest_chat_exports, {
22572
23225
  import { existsSync as existsSync23 } from "fs";
22573
23226
  import { basename as basename7, resolve as resolve9 } from "path";
22574
23227
  import { homedir as homedir8 } from "os";
22575
- import chalk24 from "chalk";
23228
+ import chalk25 from "chalk";
22576
23229
  function extractFilePath(input) {
22577
23230
  const trimmed = input.trim();
22578
23231
  const patterns = [
@@ -22607,7 +23260,7 @@ function looksLikeFilePath(input) {
22607
23260
  }
22608
23261
  async function ingestFromChat(ctx, filePath) {
22609
23262
  if (!ctx.rl) {
22610
- console.log(" " + chalk24.red("Ingest confirm requires interactive mode."));
23263
+ console.log(" " + chalk25.red("Ingest confirm requires interactive mode."));
22611
23264
  return false;
22612
23265
  }
22613
23266
  const name = basename7(filePath);
@@ -22615,7 +23268,7 @@ async function ingestFromChat(ctx, filePath) {
22615
23268
  try {
22616
23269
  const ok = await prompts.confirm(`Ingest ${name} as CRM export?`, true);
22617
23270
  if (!ok) {
22618
- console.log(" " + chalk24.dim("Ingest cancelled."));
23271
+ console.log(" " + chalk25.dim("Ingest cancelled."));
22619
23272
  return false;
22620
23273
  }
22621
23274
  } finally {
@@ -22643,7 +23296,7 @@ async function ingestFromChat(ctx, filePath) {
22643
23296
  true
22644
23297
  );
22645
23298
  if (useAi) {
22646
- console.log(" " + chalk24.dim("AI column mapping is not wired to ingest yet \u2014 trying standard ingest."));
23299
+ console.log(" " + chalk25.dim("AI column mapping is not wired to ingest yet \u2014 trying standard ingest."));
22647
23300
  }
22648
23301
  } finally {
22649
23302
  prompts2.close();
@@ -22670,7 +23323,7 @@ async function ingestFromChat(ctx, filePath) {
22670
23323
  invalidateGapAudit(ctx);
22671
23324
  saveSessionState(ctx);
22672
23325
  console.log();
22673
- console.log(" " + paint("accent", "\u2713 Data loaded") + chalk24.dim(` \u2014 ${name}`));
23326
+ console.log(" " + paint("accent", "\u2713 Data loaded") + chalk25.dim(` \u2014 ${name}`));
22674
23327
  recordMessage(ctx, "user", `[ingested ${name}]`);
22675
23328
  recordMessage(ctx, "agent", `Loaded ${name}. Checking what we can analyze\u2026`);
22676
23329
  const audit = await refreshGapAudit(ctx);
@@ -22678,7 +23331,7 @@ async function ingestFromChat(ctx, filePath) {
22678
23331
  if (audit.can_compute && ctx.scope?.confirmed_at) {
22679
23332
  if (ctx.pendingAsk) {
22680
23333
  console.log();
22681
- console.log(" " + chalk24.dim("Computing so I can answer\u2026"));
23334
+ console.log(" " + chalk25.dim("Computing so I can answer\u2026"));
22682
23335
  await runConversationCompute(ctx);
22683
23336
  return true;
22684
23337
  }
@@ -22711,11 +23364,22 @@ async function loadDemoFromChat(ctx, scenario, opts = {}) {
22711
23364
  const { resolveDemoScenarioForLoad: resolveDemoScenarioForLoad2 } = await Promise.resolve().then(() => (init_demo_fit(), demo_fit_exports));
22712
23365
  chosen = await resolveDemoScenarioForLoad2(ctx) ?? void 0;
22713
23366
  }
23367
+ if (ctx.rl) {
23368
+ const { shouldOfferInboxSkillSetup: shouldOfferInboxSkillSetup2, offerInboxSkillSetup: offerInboxSkillSetup2 } = await Promise.resolve().then(() => (init_inbox_setup(), inbox_setup_exports));
23369
+ if (shouldOfferInboxSkillSetup2("demo")) {
23370
+ const session = createPromptSession(ctx.rl, ctx);
23371
+ try {
23372
+ await offerInboxSkillSetup2(session, { beat: "demo" });
23373
+ } finally {
23374
+ session.close();
23375
+ }
23376
+ }
23377
+ }
22714
23378
  if (chosen) {
22715
23379
  const { getScenario: getScenario2 } = await Promise.resolve().then(() => (init_scenarios(), scenarios_exports));
22716
23380
  const s = getScenario2(chosen);
22717
23381
  console.log();
22718
- console.log(" " + paint("accent", "Fitting ") + s.label + chalk24.dim(" \u2014 " + s.hook));
23382
+ console.log(" " + paint("accent", "Fitting ") + s.label + chalk25.dim(" \u2014 " + s.hook));
22719
23383
  }
22720
23384
  const { handler: demo } = await Promise.resolve().then(() => (init_generate(), generate_exports));
22721
23385
  const args = ["--no-profile", "--brief"];
@@ -22751,7 +23415,7 @@ async function loadDemoFromChat(ctx, scenario, opts = {}) {
22751
23415
  const shouldAuto = opts.autoCompute || Boolean(ctx.pendingAsk && audit.can_compute && ctx.scope?.confirmed_at);
22752
23416
  if (shouldAuto && audit.can_compute) {
22753
23417
  console.log();
22754
- console.log(" " + chalk24.dim("Computing so I can answer\u2026"));
23418
+ console.log(" " + chalk25.dim("Computing so I can answer\u2026"));
22755
23419
  await runConversationCompute(ctx);
22756
23420
  return true;
22757
23421
  }
@@ -22784,7 +23448,7 @@ __export(pending_ask_exports, {
22784
23448
  queuePendingAsk: () => queuePendingAsk,
22785
23449
  resumePendingAsk: () => resumePendingAsk
22786
23450
  });
22787
- import chalk25 from "chalk";
23451
+ import chalk26 from "chalk";
22788
23452
  function looksLikeQuestion(input) {
22789
23453
  const text = input.trim();
22790
23454
  if (!text) return false;
@@ -22820,7 +23484,7 @@ function printFocusChip(ctx) {
22820
23484
  const period = ctx.scope.time_horizon ? ` \xB7 ${ctx.scope.time_horizon}` : "";
22821
23485
  console.log();
22822
23486
  console.log(
22823
- " " + chalk25.dim("Focus: ") + paint("accent", lens) + chalk25.dim(period) + chalk25.dim(" \u2014 type ") + chalk25.cyan("adjust") + chalk25.dim(" to change")
23487
+ " " + chalk26.dim("Focus: ") + paint("accent", lens) + chalk26.dim(period) + chalk26.dim(" \u2014 type ") + chalk26.cyan("adjust") + chalk26.dim(" to change")
22824
23488
  );
22825
23489
  console.log();
22826
23490
  }
@@ -22831,7 +23495,7 @@ async function resumePendingAsk(ctx) {
22831
23495
  if (canUseReplAi(ctx)) {
22832
23496
  console.log();
22833
23497
  console.log(
22834
- " " + chalk25.dim(
23498
+ " " + chalk26.dim(
22835
23499
  pending.keylessAnswered ? "Picking up your question with the connected engine\u2026" : "Picking up your question\u2026"
22836
23500
  )
22837
23501
  );
@@ -22864,7 +23528,7 @@ async function offerDemoToAnswer(ctx) {
22864
23528
  const go = await prompts.confirm("Use demo data to answer this?", true);
22865
23529
  if (!go) {
22866
23530
  console.log(
22867
- " " + chalk25.dim("Paste a CSV path when ready, or say ") + chalk25.cyan("use demo data") + chalk25.dim(".")
23531
+ " " + chalk26.dim("Paste a CSV path when ready, or say ") + chalk26.cyan("use demo data") + chalk26.dim(".")
22868
23532
  );
22869
23533
  console.log();
22870
23534
  return false;
@@ -22896,7 +23560,7 @@ __export(compute_exports2, {
22896
23560
  isComputeIntent: () => isComputeIntent,
22897
23561
  runConversationCompute: () => runConversationCompute
22898
23562
  });
22899
- import chalk26 from "chalk";
23563
+ import chalk27 from "chalk";
22900
23564
  async function runConversationCompute(ctx) {
22901
23565
  const lens = ctx.scope?.primary_lens ?? ctx.analysis.primary;
22902
23566
  ctx.computeInProgress = true;
@@ -22951,7 +23615,7 @@ async function runConversationCompute(ctx) {
22951
23615
  creditGapCompute(ctx);
22952
23616
  return typeof summary === "string" ? summary : "Health analysis ready";
22953
23617
  } catch (err) {
22954
- console.error(" " + chalk26.red(String(err.message ?? err)));
23618
+ console.error(" " + chalk27.red(String(err.message ?? err)));
22955
23619
  return;
22956
23620
  } finally {
22957
23621
  ctx.computeInProgress = false;
@@ -24355,6 +25019,11 @@ var init_lemonsqueezy = __esm({
24355
25019
 
24356
25020
  // src/license/verify.ts
24357
25021
  import { createHmac as createHmac2 } from "crypto";
25022
+ function signingSecret() {
25023
+ const secret2 = process.env.NTRP_SIGNING_SECRET;
25024
+ if (!secret2) return null;
25025
+ return secret2;
25026
+ }
24358
25027
  function validateLicenseKey(key) {
24359
25028
  const invalid = (msg) => ({
24360
25029
  valid: false,
@@ -24365,13 +25034,17 @@ function validateLicenseKey(key) {
24365
25034
  if (!key || !key.startsWith("NTRP-")) {
24366
25035
  return invalid("Invalid key format");
24367
25036
  }
25037
+ const secret2 = signingSecret();
25038
+ if (!secret2) {
25039
+ return invalid("Invalid key format");
25040
+ }
24368
25041
  const parts = key.replace("NTRP-", "").split("-");
24369
25042
  if (parts.length !== 3) {
24370
25043
  return invalid("Invalid key format");
24371
25044
  }
24372
25045
  const [payload, meta, signature] = parts;
24373
25046
  const dataToSign = `${payload}-${meta}`;
24374
- const expectedSig = createHmac2("sha256", SIGNING_SECRET).update(dataToSign).digest("hex").slice(0, 8);
25047
+ const expectedSig = createHmac2("sha256", secret2).update(dataToSign).digest("hex").slice(0, 8);
24375
25048
  if (signature !== expectedSig) {
24376
25049
  return invalid("Invalid license key");
24377
25050
  }
@@ -24468,7 +25141,6 @@ function checkLicense() {
24468
25141
  }
24469
25142
  return applyTrialPolicy(result);
24470
25143
  }
24471
- var SIGNING_SECRET;
24472
25144
  var init_verify = __esm({
24473
25145
  "src/license/verify.ts"() {
24474
25146
  "use strict";
@@ -24477,7 +25149,6 @@ var init_verify = __esm({
24477
25149
  init_upgrade_whimsy();
24478
25150
  init_normalize();
24479
25151
  init_lemonsqueezy();
24480
- SIGNING_SECRET = "ntrp-gtm-health-2026";
24481
25152
  }
24482
25153
  });
24483
25154