mneme-ai 0.9.0 โ†’ 0.11.0

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.
@@ -47,4 +47,60 @@ export interface ChatOptions {
47
47
  noLlm?: boolean;
48
48
  }
49
49
  export declare function chatCommand(opts: ChatOptions): Promise<number>;
50
+ export interface RegretOptions {
51
+ cwd: string;
52
+ windowDays?: number;
53
+ json?: boolean;
54
+ }
55
+ export declare function regretCommand(opts: RegretOptions): Promise<number>;
56
+ export interface BusFactorOptions {
57
+ cwd: string;
58
+ topN?: number;
59
+ minTouches?: number;
60
+ json?: boolean;
61
+ }
62
+ export declare function busFactorCommand(opts: BusFactorOptions): Promise<number>;
63
+ export interface ParadoxOptions {
64
+ cwd: string;
65
+ json?: boolean;
66
+ }
67
+ export declare function paradoxCommand(opts: ParadoxOptions): Promise<number>;
68
+ export interface CommitCoachOptions {
69
+ cwd: string;
70
+ diffFile?: string;
71
+ fromStdin?: boolean;
72
+ json?: boolean;
73
+ noLlm?: boolean;
74
+ }
75
+ export declare function commitCoachCommand(opts: CommitCoachOptions): Promise<number>;
76
+ export interface CrystalBallOptions {
77
+ cwd: string;
78
+ diffFile?: string;
79
+ fromStdin?: boolean;
80
+ windowDays?: number;
81
+ json?: boolean;
82
+ }
83
+ export declare function crystalBallCommand(opts: CrystalBallOptions): Promise<number>;
84
+ export interface TimeMachineOptions {
85
+ cwd: string;
86
+ filePath: string;
87
+ plateauDays?: number;
88
+ json?: boolean;
89
+ }
90
+ export declare function timeMachineCommand(opts: TimeMachineOptions): Promise<number>;
91
+ export interface PremortemOptions {
92
+ cwd: string;
93
+ intent: string;
94
+ similarityFloor?: number;
95
+ windowDays?: number;
96
+ json?: boolean;
97
+ }
98
+ export declare function premortemCommand(opts: PremortemOptions): Promise<number>;
99
+ export interface GhostOptions {
100
+ cwd: string;
101
+ topN?: number;
102
+ staleDays?: number;
103
+ json?: boolean;
104
+ }
105
+ export declare function ghostCommand(opts: GhostOptions): Promise<number>;
50
106
  //# sourceMappingURL=insights-cli.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"insights-cli.d.ts","sourceRoot":"","sources":["../../src/commands/insights-cli.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoDH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA6B5E;AA0BD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC;IACpD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAiE9E;AAID,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyFhF;AAgDD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAyGtE;AAID,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEtE;AAeD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CA4HpE"}
1
+ {"version":3,"file":"insights-cli.d.ts","sourceRoot":"","sources":["../../src/commands/insights-cli.ts"],"names":[],"mappings":"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"}
@@ -53,17 +53,40 @@ export async function whoKnowsCommand(opts) {
53
53
  return 0;
54
54
  }
55
55
  ui.banner();
56
- process.stdout.write(`\n ${kleur.bold().cyan("๐Ÿ‘ค Top experts on")} ${kleur.bold(`"${opts.topic}"`)}\n\n`);
57
- if (candidates.length === 0) {
56
+ const verdict = insights.whoKnowsVerdict(candidates);
57
+ if (opts.json) {
58
+ process.stdout.write(JSON.stringify({ topic: opts.topic, verdict, candidates }, null, 2) + "\n");
59
+ return 0;
60
+ }
61
+ // โ”€โ”€โ”€ Header โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
62
+ process.stdout.write(`\n ${kleur.bold().cyan("๐Ÿ‘ค Who knows about")} ${kleur.bold(`"${opts.topic}"`)}\n`);
63
+ process.stdout.write(` ${kleur.gray("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•")}\n\n`);
64
+ if (candidates.length === 0 || !verdict.topExpert) {
58
65
  process.stdout.write(` ${kleur.gray(`No commits matched "${opts.topic}". Try a broader topic.`)}\n\n`);
59
66
  return 0;
60
67
  }
61
- for (const c of candidates) {
62
- const tier = renderTier(c.tier);
63
- const lastTouchAge = daysAgoFromIso(c.lastTouch);
64
- process.stdout.write(` ${tier} ${kleur.bold(c.name)} ${kleur.gray(`<${c.email}>`)}\n`);
65
- process.stdout.write(` ${kleur.gray(`${c.commitCount} commits ยท ${c.filesTouched} files ยท last touch ${lastTouchAge} ago ยท score ${c.score.toFixed(2)}`)}\n\n`);
68
+ // โ”€โ”€โ”€ VERDICT (the answer the user is here for) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
69
+ const top = verdict.topExpert;
70
+ process.stdout.write(` ${kleur.bold().magenta("โœฆ Verdict")}\n\n`);
71
+ process.stdout.write(` ${kleur.bold(top.name)} ${kleur.gray(`<${top.email}>`)}\n`);
72
+ process.stdout.write(` ${kleur.cyan(verdict.confidencePct + "%")} confidence โ€” ${top.commitCount} of ${verdict.totalCommits} relevant commits\n`);
73
+ process.stdout.write(` last touch ${kleur.bold(daysAgoFromIso(top.lastTouch))} ago ยท ${top.filesTouched} files ยท ${renderTier(top.tier)}\n`);
74
+ if (verdict.risk) {
75
+ process.stdout.write(`\n ${kleur.yellow("โš  ")}${kleur.yellow(verdict.risk)}\n`);
76
+ }
77
+ if (verdict.backup) {
78
+ process.stdout.write(`\n ${kleur.gray("backup:")} ${kleur.bold(verdict.backup.name)} ${kleur.gray(`(${verdict.backup.commitCount} commits, last touch ${daysAgoFromIso(verdict.backup.lastTouch)} ago)`)}\n`);
79
+ }
80
+ // โ”€โ”€โ”€ All candidates โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
81
+ if (candidates.length > 1) {
82
+ process.stdout.write(`\n ${kleur.bold().magenta("โ—† All candidates")} ${kleur.gray(`(${candidates.length})`)}\n\n`);
83
+ for (const c of candidates) {
84
+ const tier = renderTier(c.tier);
85
+ process.stdout.write(` ${tier} ${kleur.bold(c.name)} ${kleur.gray(`<${c.email}>`)}\n`);
86
+ process.stdout.write(` ${kleur.gray(`${c.commitCount} commits ยท ${c.filesTouched} files ยท last touch ${daysAgoFromIso(c.lastTouch)} ago ยท score ${c.score.toFixed(2)}`)}\n`);
87
+ }
66
88
  }
89
+ process.stdout.write("\n");
67
90
  return 0;
68
91
  }
69
92
  function renderTier(tier) {
@@ -541,6 +564,301 @@ export async function chatCommand(opts) {
541
564
  process.stdout.write(`\n ${kleur.gray(`bye โ€” ${transcript.length} turn(s) recorded`)}\n\n`);
542
565
  return 0;
543
566
  }
567
+ // โ”€โ”€โ”€ divider helper โ€” used across all insights output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
568
+ const DIV_WIDTH = 64;
569
+ function divider(label = "") {
570
+ if (!label)
571
+ return kleur.gray("โ•".repeat(DIV_WIDTH));
572
+ const padded = `โ•โ•โ• ${label} `;
573
+ const tail = "โ•".repeat(Math.max(4, DIV_WIDTH - padded.length));
574
+ return kleur.gray(padded + tail);
575
+ }
576
+ export async function regretCommand(opts) {
577
+ const result = await withStore(opts.cwd, (s) => {
578
+ const commits = util.loadAllCommits(s);
579
+ const regrets = insights.detectRegrets(commits, { windowDays: opts.windowDays ?? 7 });
580
+ const summary = insights.summarizeRegrets(commits, regrets);
581
+ return { regrets, summary };
582
+ });
583
+ if (typeof result === "number")
584
+ return result;
585
+ const { regrets, summary } = result;
586
+ if (opts.json) {
587
+ process.stdout.write(JSON.stringify({ regrets, summary }, null, 2) + "\n");
588
+ return 0;
589
+ }
590
+ ui.banner();
591
+ process.stdout.write(`\n ${kleur.bold().cyan("๐Ÿ˜ฌ Regrets โ€” what we shipped and immediately fixed")}\n`);
592
+ process.stdout.write(` ${divider()}\n\n`);
593
+ // Verdict
594
+ process.stdout.write(` ${kleur.bold().magenta("โœฆ Summary")}\n\n`);
595
+ process.stdout.write(` ${kleur.bold(String(summary.totalRegrets))} regrets across ${kleur.bold(String(summary.totalShipped))} shipped commits ${kleur.gray(`(rate: ${(summary.regretRate * 100).toFixed(1)}%)`)}\n`);
596
+ if (summary.totalRegrets > 0) {
597
+ process.stdout.write(` average days-to-fix: ${kleur.bold(summary.averageDaysToFix.toFixed(1))}\n`);
598
+ const breakdown = Object.entries(summary.byKind)
599
+ .filter(([, n]) => n > 0)
600
+ .map(([k, n]) => `${k}: ${n}`)
601
+ .join(" ยท ");
602
+ if (breakdown)
603
+ process.stdout.write(` ${kleur.gray("breakdown: " + breakdown)}\n`);
604
+ }
605
+ if (regrets.length === 0) {
606
+ process.stdout.write(`\n ${kleur.green("โœ“")} No regrets detected โ€” clean shipping history.\n\n`);
607
+ return 0;
608
+ }
609
+ // Listing
610
+ process.stdout.write(`\n ${kleur.bold().magenta("โ—† Recent regrets")} ${kleur.gray(`(showing ${Math.min(20, regrets.length)} of ${regrets.length})`)}\n\n`);
611
+ for (const r of regrets.slice(0, 20)) {
612
+ const kindBadge = renderRegretKind(r.kind);
613
+ const shippedHash = r.shipped.shortHash || r.shipped.hash.slice(0, 7);
614
+ const followupHash = r.followup.shortHash || r.followup.hash.slice(0, 7);
615
+ process.stdout.write(` ${kindBadge} shipped ${kleur.bold(r.shipped.authorDate.slice(0, 10))} ${kleur.gray("โ†’ fixed in " + r.daysToFix + "d")}\n`);
616
+ process.stdout.write(` ${kleur.cyan(shippedHash)} ${kleur.bold(r.shipped.subject)}\n`);
617
+ process.stdout.write(` ${kleur.gray("โ†ณ " + followupHash + " " + r.followup.subject)}\n`);
618
+ if (r.lesson)
619
+ process.stdout.write(` ${kleur.gray("lesson: " + r.lesson)}\n`);
620
+ process.stdout.write("\n");
621
+ }
622
+ return 0;
623
+ }
624
+ function renderRegretKind(kind) {
625
+ switch (kind) {
626
+ case "revert":
627
+ return kleur.red().bold("REVERT ");
628
+ case "hotfix":
629
+ return kleur.yellow().bold("HOTFIX ");
630
+ case "fix":
631
+ return kleur.cyan().bold("FIX ");
632
+ default:
633
+ return kleur.gray().bold("similar ");
634
+ }
635
+ }
636
+ export async function busFactorCommand(opts) {
637
+ const result = await withStore(opts.cwd, (s) => {
638
+ return insights.busFactor(s, { topN: opts.topN ?? 20, minTouches: opts.minTouches ?? 3 });
639
+ });
640
+ if (typeof result === "number")
641
+ return result;
642
+ const risks = result;
643
+ if (opts.json) {
644
+ process.stdout.write(JSON.stringify({ risks }, null, 2) + "\n");
645
+ return 0;
646
+ }
647
+ ui.banner();
648
+ process.stdout.write(`\n ${kleur.bold().cyan("๐Ÿšจ Bus-factor risks โ€” knowledge fragility")}\n`);
649
+ process.stdout.write(` ${divider()}\n\n`);
650
+ if (risks.length === 0) {
651
+ process.stdout.write(` ${kleur.green("โœ“")} No high-risk files detected โ€” knowledge is well distributed.\n\n`);
652
+ return 0;
653
+ }
654
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
655
+ for (const r of risks)
656
+ counts[r.tier] += 1;
657
+ process.stdout.write(` ${kleur.bold().magenta("โœฆ Summary")}\n\n`);
658
+ process.stdout.write(` ${kleur.red().bold(String(counts.critical))} critical ยท ${kleur.yellow().bold(String(counts.high))} high ยท ${kleur.cyan().bold(String(counts.medium))} medium ยท ${kleur.gray(counts.low + " low")}\n\n`);
659
+ process.stdout.write(` ${kleur.bold().magenta("โ—† Files at risk")} ${kleur.gray(`(top ${risks.length})`)}\n\n`);
660
+ for (const r of risks) {
661
+ const tier = renderBusFactorTier(r.tier);
662
+ process.stdout.write(` ${tier} ${kleur.bold(r.filePath)}\n`);
663
+ process.stdout.write(` ${kleur.bold(r.topOwner.name)} ${kleur.gray(`<${r.topOwner.email}>`)} ${kleur.cyan(r.topOwner.sharePct + "%")} ${kleur.gray(`(${r.topOwner.touches} of ${r.totalTouches} commits)`)}\n`);
664
+ if (r.backup) {
665
+ process.stdout.write(` ${kleur.gray(`backup: ${r.backup.name} (${r.backup.touches} commits)`)}\n`);
666
+ }
667
+ process.stdout.write(` ${kleur.gray("โ†’ " + r.recommendation)}\n\n`);
668
+ }
669
+ return 0;
670
+ }
671
+ function renderBusFactorTier(tier) {
672
+ switch (tier) {
673
+ case "critical":
674
+ return kleur.red().bold("CRITICAL");
675
+ case "high":
676
+ return kleur.yellow().bold("HIGH ");
677
+ case "medium":
678
+ return kleur.cyan().bold("MEDIUM ");
679
+ default:
680
+ return kleur.gray().bold("LOW ");
681
+ }
682
+ }
683
+ export async function paradoxCommand(opts) {
684
+ const result = await withStore(opts.cwd, (s) => {
685
+ const commits = util.loadAllCommits(s);
686
+ const decisions = commits.flatMap(insights.extractDecisions);
687
+ return insights.detectParadoxes(decisions);
688
+ });
689
+ if (typeof result === "number")
690
+ return result;
691
+ const flipFlops = result;
692
+ if (opts.json) {
693
+ process.stdout.write(JSON.stringify({ flipFlops }, null, 2) + "\n");
694
+ return 0;
695
+ }
696
+ ui.banner();
697
+ process.stdout.write(`\n ${kleur.bold().cyan("๐ŸŒ€ Paradoxes โ€” architectural flip-flops")}\n`);
698
+ process.stdout.write(` ${divider()}\n\n`);
699
+ if (flipFlops.length === 0) {
700
+ process.stdout.write(` ${kleur.green("โœ“")} No flip-flops detected. Either the team is consistent, or commit messages are too thin to extract decisions.\n\n`);
701
+ return 0;
702
+ }
703
+ process.stdout.write(` ${kleur.bold().magenta("โœฆ Summary")}\n\n`);
704
+ process.stdout.write(` ${kleur.bold(String(flipFlops.length))} topic(s) flip-flopped over time\n\n`);
705
+ for (const f of flipFlops) {
706
+ process.stdout.write(` ${divider("topic: " + f.topic)}\n`);
707
+ process.stdout.write(` ${kleur.bold(String(f.flips))} reversal${f.flips === 1 ? "" : "s"} ${kleur.gray(`over ${f.spanMonths} months ยท ${f.chain.length} decisions`)}\n\n`);
708
+ for (const d of f.chain) {
709
+ process.stdout.write(` ${kleur.gray("โ—")} ${kleur.bold(d.date)} ${kleur.cyan(d.shortHash)} ${d.summary} ${kleur.gray(`(${d.author})`)}\n`);
710
+ }
711
+ process.stdout.write(`\n ${kleur.yellow("โ†’ ")}${kleur.yellow(f.question)}\n\n`);
712
+ }
713
+ return 0;
714
+ }
715
+ export async function commitCoachCommand(opts) {
716
+ // Read the diff: from file, stdin, or `git diff --staged`.
717
+ let diffText = "";
718
+ if (opts.diffFile) {
719
+ try {
720
+ diffText = readFileSync(opts.diffFile, "utf8");
721
+ }
722
+ catch (err) {
723
+ ui.error(`Cannot read ${opts.diffFile}: ${err.message}`);
724
+ return 1;
725
+ }
726
+ }
727
+ else if (opts.fromStdin) {
728
+ diffText = await readStdin();
729
+ }
730
+ else {
731
+ // Default: shell out to git diff --staged
732
+ try {
733
+ const { spawnSync } = await import("node:child_process");
734
+ const r = spawnSync("git", ["diff", "--staged"], { cwd: opts.cwd, encoding: "utf8" });
735
+ if (r.status === 0)
736
+ diffText = r.stdout;
737
+ }
738
+ catch {
739
+ // git not available โ€” let user know
740
+ }
741
+ }
742
+ if (!diffText.trim()) {
743
+ ui.error("No staged diff. Pass --from <file>, --stdin, or `git add` something first.");
744
+ return 1;
745
+ }
746
+ const result = await withStore(opts.cwd, (s) => insights.coach(s, diffText));
747
+ if (typeof result === "number")
748
+ return result;
749
+ const advice = result;
750
+ if (opts.json) {
751
+ process.stdout.write(JSON.stringify(advice, null, 2) + "\n");
752
+ return 0;
753
+ }
754
+ ui.banner();
755
+ process.stdout.write(`\n ${kleur.bold().cyan("๐Ÿชถ Commit coach โ€” pre-commit review")}\n`);
756
+ process.stdout.write(` ${divider()}\n\n`);
757
+ // Diff section
758
+ process.stdout.write(` ${kleur.bold().magenta("โœฆ Diff")}\n\n`);
759
+ process.stdout.write(` ${kleur.bold(String(advice.diff.files.length))} file(s) ยท ${kleur.green("+" + advice.diff.added)} ${kleur.red("-" + advice.diff.removed)} ยท shape: ${kleur.cyan(advice.diff.shape)}\n`);
760
+ for (const m of advice.diff.modules.slice(0, 5))
761
+ process.stdout.write(` ${kleur.gray("ยท")} ${m}\n`);
762
+ process.stdout.write("\n");
763
+ // Suggested message
764
+ process.stdout.write(` ${kleur.bold().magenta("โœฆ Suggested commit message")}\n\n`);
765
+ process.stdout.write(` ${kleur.bold(advice.suggestedSubject)}\n\n`);
766
+ // Reviewers
767
+ if (advice.reviewers.length > 0) {
768
+ process.stdout.write(` ${kleur.bold().magenta("โ—† Reviewers")} ${kleur.gray("(top experts on touched files)")}\n\n`);
769
+ for (const r of advice.reviewers) {
770
+ process.stdout.write(` ${kleur.cyan("โ—")} ${kleur.bold(r.name)} ${kleur.gray(`<${r.email}>`)} ${kleur.cyan(r.ownership + "%")} ${kleur.gray(`(${r.ownedFiles.length} owned files)`)}\n`);
771
+ }
772
+ process.stdout.write("\n");
773
+ }
774
+ // Scope
775
+ process.stdout.write(` ${kleur.bold().magenta("โ—† Scope")}\n\n`);
776
+ process.stdout.write(` ${advice.scopeOK ? kleur.green("โœ“") : kleur.yellow("โš ")} ${advice.scopeMessage}\n\n`);
777
+ // Warnings
778
+ if (advice.warnings.length > 0) {
779
+ process.stdout.write(` ${kleur.bold().magenta("โš  Past warnings")}\n\n`);
780
+ for (const w of advice.warnings) {
781
+ process.stdout.write(` ${kleur.yellow("โš  ")}${kleur.yellow(w.pattern)}\n`);
782
+ process.stdout.write(` ${kleur.gray(`${w.pastDate} ยท ${w.pastCommitHash} ยท ${w.outcome}`)}\n\n`);
783
+ }
784
+ }
785
+ else {
786
+ process.stdout.write(` ${kleur.green("โœ“")} ${kleur.gray("No past-regret warnings for these files.")}\n\n`);
787
+ }
788
+ return 0;
789
+ }
790
+ export async function crystalBallCommand(opts) {
791
+ // Same diff-acquisition pattern as commit-coach.
792
+ let diffText = "";
793
+ if (opts.diffFile) {
794
+ try {
795
+ diffText = readFileSync(opts.diffFile, "utf8");
796
+ }
797
+ catch (err) {
798
+ ui.error(`Cannot read ${opts.diffFile}: ${err.message}`);
799
+ return 1;
800
+ }
801
+ }
802
+ else if (opts.fromStdin) {
803
+ diffText = await readStdin();
804
+ }
805
+ else {
806
+ try {
807
+ const { spawnSync } = await import("node:child_process");
808
+ const r = spawnSync("git", ["diff", "--staged"], { cwd: opts.cwd, encoding: "utf8" });
809
+ if (r.status === 0)
810
+ diffText = r.stdout;
811
+ }
812
+ catch {
813
+ /* ignore */
814
+ }
815
+ }
816
+ if (!diffText.trim()) {
817
+ ui.error("No staged diff. Pass --from <file>, --stdin, or `git add` something first.");
818
+ return 1;
819
+ }
820
+ const result = await withStore(opts.cwd, (s) => insights.predict(s, diffText, opts.windowDays ?? 14));
821
+ if (typeof result === "number")
822
+ return result;
823
+ const p = result;
824
+ if (opts.json) {
825
+ process.stdout.write(JSON.stringify(p, null, 2) + "\n");
826
+ return 0;
827
+ }
828
+ ui.banner();
829
+ process.stdout.write(`\n ${kleur.bold().cyan("๐Ÿ”ฎ Crystal ball โ€” CI / follow-up failure prediction")}\n`);
830
+ process.stdout.write(` ${divider()}\n\n`);
831
+ // Verdict
832
+ process.stdout.write(` ${kleur.bold().magenta("โœฆ Verdict")}\n\n`);
833
+ process.stdout.write(` ${renderCrystalBallVerdict(p.verdict)} ${kleur.cyan((p.pClean * 100).toFixed(0) + "%")} clean rate ${kleur.gray(`(${p.cleanN}/${p.similarN} similar past changes)`)}\n\n`);
834
+ // Fingerprint
835
+ process.stdout.write(` ${kleur.bold().magenta("โ—† Diff fingerprint")}\n\n`);
836
+ process.stdout.write(` modules: ${p.fingerprint.modules.join(", ") || kleur.gray("(none)")}\n`);
837
+ process.stdout.write(` extensions: ${p.fingerprint.extensions.join(", ") || kleur.gray("(none)")}\n`);
838
+ process.stdout.write(` shape: ${kleur.cyan(p.fingerprint.shape)} ยท ${p.fingerprint.size} ยท tests ${p.fingerprint.hasTests ? "yes" : "no"}\n\n`);
839
+ // Most-similar
840
+ if (p.mostSimilar) {
841
+ const outcome = p.mostSimilar.outcome === "clean" ? kleur.green("clean") : kleur.red("trouble");
842
+ process.stdout.write(` ${kleur.bold().magenta("โ—† Most similar past change")}\n\n`);
843
+ process.stdout.write(` ${kleur.bold(p.mostSimilar.hash)} ${kleur.gray(p.mostSimilar.date)} ${p.mostSimilar.subject}\n`);
844
+ process.stdout.write(` outcome: ${outcome}\n\n`);
845
+ }
846
+ process.stdout.write(` ${kleur.bold().magenta("โ†’ Recommendation")}\n\n`);
847
+ process.stdout.write(` ${p.recommendation}\n\n`);
848
+ return 0;
849
+ }
850
+ function renderCrystalBallVerdict(v) {
851
+ switch (v) {
852
+ case "clear":
853
+ return kleur.green().bold("โ— CLEAR ");
854
+ case "moderate":
855
+ return kleur.yellow().bold("โ— MODERATE ");
856
+ case "risky":
857
+ return kleur.red().bold("โ— RISKY ");
858
+ default:
859
+ return kleur.gray().bold("โ—‹ UNKNOWN ");
860
+ }
861
+ }
544
862
  async function narrateAct(enricher, topic, commits) {
545
863
  if (commits.length === 0)
546
864
  return "";
@@ -576,4 +894,183 @@ function wrap(text, width, indent) {
576
894
  out.push(line);
577
895
  return out;
578
896
  }
897
+ export async function timeMachineCommand(opts) {
898
+ const result = await withStore(opts.cwd, (s) => {
899
+ const allCommits = util.loadAllCommits(s);
900
+ // Filter to commits that touched the target file
901
+ const target = opts.filePath;
902
+ const filtered = allCommits.filter((c) => c.files.includes(target));
903
+ const fileChanges = util.loadFileChangesForPath(s, target);
904
+ const changeMap = new Map();
905
+ for (const ch of fileChanges)
906
+ changeMap.set(ch.commitHash, ch);
907
+ return insights.buildTimeMachine(target, filtered, changeMap, {
908
+ plateauDays: opts.plateauDays ?? 60,
909
+ });
910
+ });
911
+ if (typeof result === "number")
912
+ return result;
913
+ if (opts.json) {
914
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
915
+ return 0;
916
+ }
917
+ ui.banner();
918
+ process.stdout.write(`\n ${kleur.bold().cyan("๐Ÿ•ฐ Time Machine โ€” life of a file")}\n`);
919
+ process.stdout.write(` ${divider()}\n\n`);
920
+ process.stdout.write(` ${kleur.bold(opts.filePath)}\n`);
921
+ if (result.totalCommits === 0) {
922
+ process.stdout.write(`\n ${kleur.yellow("โœ—")} No commits found for this path. Check the file exists in history.\n\n`);
923
+ return 0;
924
+ }
925
+ process.stdout.write(` ${kleur.gray(String(result.totalCommits) + " commits across " + result.totalSpanDays + " days")}\n\n`);
926
+ // Health line
927
+ const h = result.health;
928
+ const pct = (n) => Math.round(n * 100);
929
+ process.stdout.write(` ${kleur.bold().magenta("โœฆ Health")}\n\n`);
930
+ process.stdout.write(` rewrite ${kleur.bold(pct(h.rewriteRatio) + "%")} ยท firefight ${kleur.bold(pct(h.firefightRatio) + "%")} ยท polish/plateau ${kleur.bold(pct(h.polishRatio) + "%")}\n\n`);
931
+ // Epochs timeline
932
+ process.stdout.write(` ${kleur.bold().magenta("โ—† Epochs")}\n\n`);
933
+ for (const e of result.epochs) {
934
+ const badge = renderEpochKind(e.kind);
935
+ const range = e.fromDate === e.toDate ? e.fromDate : `${e.fromDate} โ†’ ${e.toDate}`;
936
+ process.stdout.write(` ${badge} ${kleur.gray(range)} ${kleur.gray(`(${e.spanDays}d)`)}\n`);
937
+ process.stdout.write(` ${e.label}\n`);
938
+ if (e.commits.length > 0) {
939
+ const churn = e.insertions + e.deletions;
940
+ process.stdout.write(` ${kleur.gray(`${e.commits.length} commits ยท +${e.insertions}/-${e.deletions} (${churn} lines)`)}\n`);
941
+ }
942
+ process.stdout.write("\n");
943
+ }
944
+ return 0;
945
+ }
946
+ function renderEpochKind(kind) {
947
+ switch (kind) {
948
+ case "birth":
949
+ return kleur.green().bold("BIRTH ");
950
+ case "rewrite":
951
+ return kleur.magenta().bold("REWRITE ");
952
+ case "evolution":
953
+ return kleur.cyan().bold("EVOLVE ");
954
+ case "firefight":
955
+ return kleur.red().bold("FIREFIGHT");
956
+ case "polish":
957
+ return kleur.blue().bold("POLISH ");
958
+ case "plateau":
959
+ return kleur.gray().bold("PLATEAU ");
960
+ case "twilight":
961
+ return kleur.gray().bold("TWILIGHT ");
962
+ default:
963
+ return kleur.gray().bold(kind.padEnd(9));
964
+ }
965
+ }
966
+ export async function premortemCommand(opts) {
967
+ const result = await withStore(opts.cwd, (s) => {
968
+ const commits = util.loadAllCommits(s);
969
+ return insights.buildPremortem(opts.intent, commits, {
970
+ similarityFloor: opts.similarityFloor ?? 0.25,
971
+ windowDays: opts.windowDays ?? 14,
972
+ });
973
+ });
974
+ if (typeof result === "number")
975
+ return result;
976
+ if (opts.json) {
977
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
978
+ return 0;
979
+ }
980
+ ui.banner();
981
+ process.stdout.write(`\n ${kleur.bold().cyan("๐Ÿ”ฎ Pre-mortem โ€” what your repo's history says about this")}\n`);
982
+ process.stdout.write(` ${divider()}\n\n`);
983
+ process.stdout.write(` ${kleur.gray("intent:")} ${kleur.bold(opts.intent)}\n\n`);
984
+ const verdictColor = (() => {
985
+ switch (result.verdict) {
986
+ case "very_high": return kleur.red().bold;
987
+ case "high": return kleur.yellow().bold;
988
+ case "medium": return kleur.cyan().bold;
989
+ case "low": return kleur.green().bold;
990
+ }
991
+ })();
992
+ process.stdout.write(` ${kleur.bold().magenta("โœฆ Verdict")}\n\n`);
993
+ process.stdout.write(` risk: ${verdictColor(result.verdict.toUpperCase().replace("_", " "))} ${kleur.gray(`(P(regret) = ${(result.regretProbability * 100).toFixed(0)}%)`)}\n\n`);
994
+ for (const line of wrap(result.summary, 76, " ")) {
995
+ process.stdout.write(line + "\n");
996
+ }
997
+ if (result.topRisks.length > 0) {
998
+ process.stdout.write(`\n ${kleur.bold().magenta("โ—† Top risks")}\n\n`);
999
+ for (const r of result.topRisks) {
1000
+ process.stdout.write(` ${kleur.bold("โ€ข")} ${r.label}\n`);
1001
+ for (const ev of r.evidence.slice(0, 3)) {
1002
+ const hash = ev.shortHash || ev.hash.slice(0, 7);
1003
+ process.stdout.write(` ${kleur.gray(hash + " " + ev.subject)}\n`);
1004
+ }
1005
+ }
1006
+ }
1007
+ if (result.pastAttempts.length > 0) {
1008
+ const sample = result.pastAttempts.slice(0, 5);
1009
+ process.stdout.write(`\n ${kleur.bold().magenta("โ—‡ Similar past attempts")} ${kleur.gray(`(${result.pastAttempts.length} found)`)}\n\n`);
1010
+ for (const a of sample) {
1011
+ const hash = a.attempt.shortHash || a.attempt.hash.slice(0, 7);
1012
+ const status = a.riskKind === "none"
1013
+ ? kleur.green("ok")
1014
+ : kleur.red(a.riskKind);
1015
+ process.stdout.write(` ${kleur.gray(a.attempt.authorDate.slice(0, 10))} ${kleur.cyan(hash)} [${status}] ${a.attempt.subject}\n`);
1016
+ }
1017
+ }
1018
+ process.stdout.write("\n");
1019
+ return 0;
1020
+ }
1021
+ export async function ghostCommand(opts) {
1022
+ const result = await withStore(opts.cwd, (s) => {
1023
+ const commits = util.loadAllCommits(s);
1024
+ const changes = util.loadAllFileChanges(s);
1025
+ return insights.buildGhostReport(commits, changes, {
1026
+ staleDays: opts.staleDays ?? 180,
1027
+ minGhostliness: 0.4,
1028
+ });
1029
+ });
1030
+ if (typeof result === "number")
1031
+ return result;
1032
+ if (opts.json) {
1033
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1034
+ return 0;
1035
+ }
1036
+ ui.banner();
1037
+ process.stdout.write(`\n ${kleur.bold().cyan("๐Ÿ‘ป Ghost Code โ€” what's haunting your repo")}\n`);
1038
+ process.stdout.write(` ${divider()}\n\n`);
1039
+ process.stdout.write(` ${kleur.bold(String(result.totalFiles))} files analyzed ยท ${kleur.bold(result.ghostFiles.length + " ghosts")} surfaced ยท avg ghostliness ${kleur.bold((result.averageGhostliness * 100).toFixed(0) + "%")}\n\n`);
1040
+ if (result.ghostFiles.length === 0) {
1041
+ process.stdout.write(` ${kleur.green("โœ“")} Repo looks active โ€” no significant ghost code detected.\n\n`);
1042
+ return 0;
1043
+ }
1044
+ process.stdout.write(` ${kleur.bold().magenta("โ—† Ghost files")} ${kleur.gray(`(top ${Math.min(opts.topN ?? 10, result.ghostFiles.length)})`)}\n\n`);
1045
+ for (const g of result.ghostFiles.slice(0, opts.topN ?? 10)) {
1046
+ const score = (g.ghostliness * 100).toFixed(0) + "%";
1047
+ const meter = renderMeter(g.ghostliness);
1048
+ process.stdout.write(` ${kleur.bold(g.path)}\n`);
1049
+ process.stdout.write(` ${meter} ${kleur.bold(score)} ${kleur.gray(g.reason)}\n`);
1050
+ process.stdout.write(` ${kleur.gray(`${g.totalCommits} commits ยท ${g.daysSinceLastTouch}d quiet ยท last: "${truncateOneLine(g.lastCommitSubject, 60)}"`)}\n\n`);
1051
+ }
1052
+ if (result.staleTodos.length > 0) {
1053
+ process.stdout.write(` ${kleur.bold().magenta("โ—‡ Stale TODOs")} ${kleur.gray(`(${result.staleTodos.length} ignored markers)`)}\n\n`);
1054
+ for (const t of result.staleTodos.slice(0, 5)) {
1055
+ process.stdout.write(` ${kleur.bold(t.filePath)}\n`);
1056
+ process.stdout.write(` ${kleur.gray(`${t.ageDays}d old ยท ignored ${t.ignoredCount}ร— since`)}\n`);
1057
+ process.stdout.write(` ${kleur.gray("โ†ณ " + truncateOneLine(t.hint, 70))}\n\n`);
1058
+ }
1059
+ }
1060
+ return 0;
1061
+ }
1062
+ function renderMeter(value) {
1063
+ const blocks = Math.round(value * 10);
1064
+ const filled = "โ–ˆ".repeat(blocks);
1065
+ const empty = "โ–‘".repeat(10 - blocks);
1066
+ if (value >= 0.7)
1067
+ return kleur.red(filled) + kleur.gray(empty);
1068
+ if (value >= 0.4)
1069
+ return kleur.yellow(filled) + kleur.gray(empty);
1070
+ return kleur.green(filled) + kleur.gray(empty);
1071
+ }
1072
+ function truncateOneLine(s, n) {
1073
+ const oneLine = s.replace(/\s+/g, " ").trim();
1074
+ return oneLine.length <= n ? oneLine : oneLine.slice(0, n - 1).trimEnd() + "โ€ฆ";
1075
+ }
579
1076
  //# sourceMappingURL=insights-cli.js.map