@rely-ai/caliber 1.20.0-dev.1773687255 → 1.20.0-dev.1773690658

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 (2) hide show
  1. package/dist/bin.js +304 -35
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -149,13 +149,14 @@ __export(constants_exports, {
149
149
  CALIBER_DIR: () => CALIBER_DIR,
150
150
  LEARNING_DIR: () => LEARNING_DIR,
151
151
  LEARNING_MAX_EVENTS: () => LEARNING_MAX_EVENTS,
152
+ LEARNING_ROI_FILE: () => LEARNING_ROI_FILE,
152
153
  LEARNING_SESSION_FILE: () => LEARNING_SESSION_FILE,
153
154
  LEARNING_STATE_FILE: () => LEARNING_STATE_FILE,
154
155
  MANIFEST_FILE: () => MANIFEST_FILE
155
156
  });
156
157
  import path9 from "path";
157
158
  import os2 from "os";
158
- var AUTH_DIR, CALIBER_DIR, MANIFEST_FILE, BACKUPS_DIR, LEARNING_DIR, LEARNING_SESSION_FILE, LEARNING_STATE_FILE, LEARNING_MAX_EVENTS;
159
+ var AUTH_DIR, CALIBER_DIR, MANIFEST_FILE, BACKUPS_DIR, LEARNING_DIR, LEARNING_SESSION_FILE, LEARNING_STATE_FILE, LEARNING_MAX_EVENTS, LEARNING_ROI_FILE;
159
160
  var init_constants = __esm({
160
161
  "src/constants.ts"() {
161
162
  "use strict";
@@ -167,6 +168,7 @@ var init_constants = __esm({
167
168
  LEARNING_SESSION_FILE = "current-session.jsonl";
168
169
  LEARNING_STATE_FILE = "state.json";
169
170
  LEARNING_MAX_EVENTS = 500;
171
+ LEARNING_ROI_FILE = "roi-stats.json";
170
172
  }
171
173
  });
172
174
 
@@ -219,8 +221,8 @@ var init_lock = __esm({
219
221
 
220
222
  // src/cli.ts
221
223
  import { Command } from "commander";
222
- import fs33 from "fs";
223
- import path26 from "path";
224
+ import fs34 from "fs";
225
+ import path27 from "path";
224
226
  import { fileURLToPath } from "url";
225
227
 
226
228
  // src/commands/init.ts
@@ -2240,15 +2242,15 @@ init_config();
2240
2242
  // src/utils/dependencies.ts
2241
2243
  import { readFileSync } from "fs";
2242
2244
  import { join } from "path";
2243
- function readFileOrNull(path28) {
2245
+ function readFileOrNull(path29) {
2244
2246
  try {
2245
- return readFileSync(path28, "utf-8");
2247
+ return readFileSync(path29, "utf-8");
2246
2248
  } catch {
2247
2249
  return null;
2248
2250
  }
2249
2251
  }
2250
- function readJsonOrNull(path28) {
2251
- const content = readFileOrNull(path28);
2252
+ function readJsonOrNull(path29) {
2253
+ const content = readFileOrNull(path29);
2252
2254
  if (!content) return null;
2253
2255
  try {
2254
2256
  return JSON.parse(content);
@@ -5329,12 +5331,12 @@ var AGENT_DISPLAY_NAMES = {
5329
5331
  codex: "Codex"
5330
5332
  };
5331
5333
  var CATEGORY_LABELS = {
5332
- existence: "FILES & SETUP",
5333
- quality: "QUALITY",
5334
- grounding: "GROUNDING",
5335
- accuracy: "ACCURACY",
5336
- freshness: "FRESHNESS & SAFETY",
5337
- bonus: "BONUS"
5334
+ existence: { icon: "\u{1F4C1}", label: "FILES & SETUP" },
5335
+ quality: { icon: "\u26A1", label: "QUALITY" },
5336
+ grounding: { icon: "\u{1F3AF}", label: "GROUNDING" },
5337
+ accuracy: { icon: "\u{1F50D}", label: "ACCURACY" },
5338
+ freshness: { icon: "\u{1F6E1}\uFE0F", label: "FRESHNESS & SAFETY" },
5339
+ bonus: { icon: "\u2B50", label: "BONUS" }
5338
5340
  };
5339
5341
  var CATEGORY_ORDER = ["existence", "quality", "grounding", "accuracy", "freshness", "bonus"];
5340
5342
  function gradeColor(grade) {
@@ -5353,20 +5355,51 @@ function gradeColor(grade) {
5353
5355
  return chalk6.white;
5354
5356
  }
5355
5357
  }
5358
+ var GRADIENT_COLORS = ["#ef4444", "#f97316", "#eab308", "#22c55e"];
5356
5359
  function progressBar(score, max, width = 40) {
5357
5360
  const filled = Math.round(score / max * width);
5358
5361
  const empty = width - filled;
5359
- const bar = chalk6.hex("#f97316")("\u2593".repeat(filled)) + chalk6.gray("\u2591".repeat(empty));
5362
+ let bar = "";
5363
+ for (let i = 0; i < filled; i++) {
5364
+ const position = i / (width - 1);
5365
+ const colorIndex = Math.min(
5366
+ GRADIENT_COLORS.length - 1,
5367
+ Math.floor(position * GRADIENT_COLORS.length)
5368
+ );
5369
+ bar += chalk6.hex(GRADIENT_COLORS[colorIndex])("\u2593");
5370
+ }
5371
+ bar += chalk6.gray("\u2591".repeat(empty));
5360
5372
  return bar;
5361
5373
  }
5362
5374
  function formatCheck(check) {
5363
- const icon = check.passed ? chalk6.green("\u2713") : check.earnedPoints < 0 ? chalk6.red("\u2717") : chalk6.gray("\u2717");
5364
- const points = check.passed ? chalk6.green(`+${check.earnedPoints}`.padStart(4)) : check.earnedPoints < 0 ? chalk6.red(`${check.earnedPoints}`.padStart(4)) : chalk6.gray(" \u2014");
5365
- const name = check.passed ? chalk6.white(check.name) : chalk6.gray(check.name);
5375
+ const isPartial = !check.passed && check.earnedPoints > 0;
5376
+ const isNegative = check.earnedPoints < 0;
5377
+ const lostPoints = check.maxPoints - check.earnedPoints;
5378
+ const icon = check.passed ? chalk6.green("\u2713") : isPartial ? chalk6.yellow("~") : isNegative ? chalk6.red("\u2717") : chalk6.gray("\u2717");
5379
+ let points;
5380
+ if (check.passed) {
5381
+ points = chalk6.green(`+${check.earnedPoints}`.padStart(4));
5382
+ } else if (isNegative) {
5383
+ points = chalk6.red(`${check.earnedPoints}`.padStart(4));
5384
+ } else if (isPartial) {
5385
+ points = chalk6.yellow(`${check.earnedPoints}/${check.maxPoints}`.padStart(5));
5386
+ } else {
5387
+ points = chalk6.gray(`0/${check.maxPoints}`.padStart(5));
5388
+ }
5389
+ const name = check.passed ? chalk6.white(check.name) : isNegative ? chalk6.red(check.name) : isPartial ? chalk6.white(check.name) : chalk6.gray(check.name);
5366
5390
  const detail = check.detail ? chalk6.gray(` (${check.detail})`) : "";
5367
- const suggestion = !check.passed && check.suggestion ? chalk6.gray(`
5368
- \u2192 ${check.suggestion}`) : "";
5369
- return ` ${icon} ${name.padEnd(38)}${points}${detail}${suggestion}`;
5391
+ let suggestion = "";
5392
+ if (!check.passed && check.suggestion) {
5393
+ const suggColor = isNegative ? chalk6.red : chalk6.yellow;
5394
+ suggestion = suggColor(`
5395
+ \u2192 ${check.suggestion}`);
5396
+ }
5397
+ let recovery = "";
5398
+ if (isPartial && lostPoints > 0) {
5399
+ recovery = chalk6.yellow(`
5400
+ \u2191 Fix this for +${lostPoints} more points`);
5401
+ }
5402
+ return ` ${icon} ${name.padEnd(38)}${points}${detail}${suggestion}${recovery}`;
5370
5403
  }
5371
5404
  function displayScore(result) {
5372
5405
  const gc = gradeColor(result.grade);
@@ -5383,14 +5416,32 @@ function displayScore(result) {
5383
5416
  for (const category of CATEGORY_ORDER) {
5384
5417
  const summary = result.categories[category];
5385
5418
  const categoryChecks = result.checks.filter((c) => c.category === category);
5419
+ const { icon, label } = CATEGORY_LABELS[category];
5420
+ const gap = summary.max - summary.earned;
5421
+ const gapLabel = gap > 0 ? chalk6.yellow(` (-${gap} available)`) : "";
5386
5422
  console.log(
5387
- chalk6.gray(` ${CATEGORY_LABELS[category]}`) + chalk6.gray(" ".repeat(Math.max(1, 45 - CATEGORY_LABELS[category].length))) + chalk6.white(`${summary.earned}`) + chalk6.gray(` / ${summary.max}`)
5423
+ chalk6.gray(` ${icon} ${label}`) + chalk6.gray(" ".repeat(Math.max(1, 43 - label.length))) + chalk6.white(`${summary.earned}`) + chalk6.gray(` / ${summary.max}`) + gapLabel
5388
5424
  );
5389
5425
  for (const check of categoryChecks) {
5390
5426
  console.log(formatCheck(check));
5391
5427
  }
5392
5428
  console.log("");
5393
5429
  }
5430
+ formatTopImprovements(result.checks);
5431
+ }
5432
+ function formatTopImprovements(checks) {
5433
+ const improvable = checks.filter((c) => c.earnedPoints < c.maxPoints).map((c) => ({ name: c.name, potential: c.maxPoints - c.earnedPoints })).sort((a, b) => b.potential - a.potential).slice(0, 5);
5434
+ if (improvable.length === 0) return;
5435
+ console.log(chalk6.gray(" \u2500 TOP IMPROVEMENTS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5436
+ console.log("");
5437
+ for (let i = 0; i < improvable.length; i++) {
5438
+ const item = improvable[i];
5439
+ const num = chalk6.gray(`${i + 1}.`);
5440
+ const label = chalk6.white(item.name.padEnd(42));
5441
+ const pts = chalk6.yellow(`+${item.potential} pts`);
5442
+ console.log(` ${num} ${label}${pts}`);
5443
+ }
5444
+ console.log("");
5394
5445
  }
5395
5446
  function displayScoreSummary(result) {
5396
5447
  const gc = gradeColor(result.grade);
@@ -5757,6 +5808,36 @@ function trackSkillsInstalled(count) {
5757
5808
  function trackUndoExecuted() {
5758
5809
  trackEvent("undo_executed");
5759
5810
  }
5811
+ function trackLearnSessionAnalyzed(props) {
5812
+ trackEvent("learn_session_analyzed", {
5813
+ event_count: props.eventCount,
5814
+ failure_count: props.failureCount,
5815
+ correction_count: props.correctionCount,
5816
+ had_learnings_available: props.hadLearningsAvailable,
5817
+ learnings_available_count: props.learningsAvailableCount,
5818
+ new_learnings_produced: props.newLearningsProduced,
5819
+ waste_tokens: props.wasteTokens
5820
+ });
5821
+ }
5822
+ function trackLearnROISnapshot(props) {
5823
+ trackEvent("learn_roi_snapshot", {
5824
+ total_waste_tokens: props.totalWasteTokens,
5825
+ total_sessions: props.totalSessions,
5826
+ sessions_with_learnings: props.sessionsWithLearnings,
5827
+ sessions_without_learnings: props.sessionsWithoutLearnings,
5828
+ failure_rate_with_learnings: props.failureRateWithLearnings,
5829
+ failure_rate_without_learnings: props.failureRateWithoutLearnings,
5830
+ estimated_savings_tokens: props.estimatedSavingsTokens,
5831
+ learning_count: props.learningCount
5832
+ });
5833
+ }
5834
+ function trackLearnNewLearning(props) {
5835
+ trackEvent("learn_new_learning", {
5836
+ observation_type: props.observationType,
5837
+ waste_tokens: props.wasteTokens,
5838
+ source_event_count: props.sourceEventCount
5839
+ });
5840
+ }
5760
5841
 
5761
5842
  // src/commands/recommend.ts
5762
5843
  function detectLocalPlatforms() {
@@ -7084,7 +7165,7 @@ async function refineLoop(currentSetup, _targetAgent, sessionHistory) {
7084
7165
  }
7085
7166
  function summarizeSetup(action, setup) {
7086
7167
  const descriptions = setup.fileDescriptions;
7087
- const files = descriptions ? Object.entries(descriptions).map(([path28, desc]) => ` ${path28}: ${desc}`).join("\n") : Object.keys(setup).filter((k) => k !== "targetAgent" && k !== "fileDescriptions").join(", ");
7168
+ const files = descriptions ? Object.entries(descriptions).map(([path29, desc]) => ` ${path29}: ${desc}`).join("\n") : Object.keys(setup).filter((k) => k !== "targetAgent" && k !== "fileDescriptions").join(", ");
7088
7169
  return `${action}. Files:
7089
7170
  ${files}`;
7090
7171
  }
@@ -8285,7 +8366,7 @@ async function configCommand() {
8285
8366
  }
8286
8367
 
8287
8368
  // src/commands/learn.ts
8288
- import fs32 from "fs";
8369
+ import fs33 from "fs";
8289
8370
  import chalk18 from "chalk";
8290
8371
 
8291
8372
  // src/learner/stdin.ts
@@ -8555,9 +8636,124 @@ ${eventsText}`;
8555
8636
  });
8556
8637
  return parseAnalysisResponse(raw);
8557
8638
  }
8639
+ function calculateSessionWaste(events) {
8640
+ let totalWasteTokens = 0;
8641
+ let failureCount = 0;
8642
+ let promptCount = 0;
8643
+ for (const event of events) {
8644
+ if (event.hook_event_name === "PostToolUseFailure") {
8645
+ const te = event;
8646
+ const inputStr = JSON.stringify(te.tool_input);
8647
+ const responseStr = typeof te.tool_response === "object" && "_truncated" in te.tool_response ? String(te.tool_response._truncated) : JSON.stringify(te.tool_response);
8648
+ totalWasteTokens += estimateTokens(inputStr + responseStr);
8649
+ failureCount++;
8650
+ } else if (event.hook_event_name === "UserPromptSubmit") {
8651
+ promptCount++;
8652
+ }
8653
+ }
8654
+ return { totalWasteTokens, failureCount, promptCount };
8655
+ }
8558
8656
 
8559
8657
  // src/commands/learn.ts
8560
8658
  init_config();
8659
+
8660
+ // src/learner/roi.ts
8661
+ init_constants();
8662
+ import fs32 from "fs";
8663
+ import path26 from "path";
8664
+ var DEFAULT_TOTALS = {
8665
+ totalWasteTokens: 0,
8666
+ totalSessionsWithLearnings: 0,
8667
+ totalSessionsWithoutLearnings: 0,
8668
+ totalFailuresWithLearnings: 0,
8669
+ totalFailuresWithoutLearnings: 0,
8670
+ estimatedSavingsTokens: 0,
8671
+ firstSessionTimestamp: "",
8672
+ lastSessionTimestamp: ""
8673
+ };
8674
+ function roiFilePath() {
8675
+ return path26.join(LEARNING_DIR, LEARNING_ROI_FILE);
8676
+ }
8677
+ function readROIStats() {
8678
+ const filePath = roiFilePath();
8679
+ if (!fs32.existsSync(filePath)) {
8680
+ return { learnings: [], sessions: [], totals: { ...DEFAULT_TOTALS } };
8681
+ }
8682
+ try {
8683
+ return JSON.parse(fs32.readFileSync(filePath, "utf-8"));
8684
+ } catch {
8685
+ return { learnings: [], sessions: [], totals: { ...DEFAULT_TOTALS } };
8686
+ }
8687
+ }
8688
+ function writeROIStats(stats) {
8689
+ ensureLearningDir();
8690
+ fs32.writeFileSync(roiFilePath(), JSON.stringify(stats, null, 2));
8691
+ }
8692
+ function recalculateTotals(stats) {
8693
+ const totals = stats.totals;
8694
+ totals.totalWasteTokens = stats.learnings.reduce((sum, l) => sum + l.wasteTokens, 0);
8695
+ totals.totalSessionsWithLearnings = 0;
8696
+ totals.totalSessionsWithoutLearnings = 0;
8697
+ totals.totalFailuresWithLearnings = 0;
8698
+ totals.totalFailuresWithoutLearnings = 0;
8699
+ for (const s of stats.sessions) {
8700
+ if (s.hadLearningsAvailable) {
8701
+ totals.totalSessionsWithLearnings++;
8702
+ totals.totalFailuresWithLearnings += s.failureCount;
8703
+ } else {
8704
+ totals.totalSessionsWithoutLearnings++;
8705
+ totals.totalFailuresWithoutLearnings += s.failureCount;
8706
+ }
8707
+ }
8708
+ totals.estimatedSavingsTokens = totals.totalWasteTokens * totals.totalSessionsWithLearnings;
8709
+ if (stats.sessions.length > 0) {
8710
+ totals.firstSessionTimestamp = stats.sessions[0].timestamp;
8711
+ totals.lastSessionTimestamp = stats.sessions[stats.sessions.length - 1].timestamp;
8712
+ }
8713
+ }
8714
+ var MAX_SESSIONS = 500;
8715
+ var MAX_LEARNINGS = 1e3;
8716
+ function recordSession(summary, learnings) {
8717
+ const stats = readROIStats();
8718
+ stats.sessions.push(summary);
8719
+ if (learnings?.length) {
8720
+ stats.learnings.push(...learnings);
8721
+ }
8722
+ if (stats.sessions.length > MAX_SESSIONS) {
8723
+ stats.sessions = stats.sessions.slice(-MAX_SESSIONS);
8724
+ }
8725
+ if (stats.learnings.length > MAX_LEARNINGS) {
8726
+ stats.learnings = stats.learnings.slice(-MAX_LEARNINGS);
8727
+ }
8728
+ recalculateTotals(stats);
8729
+ writeROIStats(stats);
8730
+ return stats;
8731
+ }
8732
+ function formatROISummary(stats) {
8733
+ const t = stats.totals;
8734
+ const totalSessions = t.totalSessionsWithLearnings + t.totalSessionsWithoutLearnings;
8735
+ if (totalSessions === 0) return "";
8736
+ const lines = ["ROI Summary"];
8737
+ lines.push(` Sessions tracked: ${totalSessions}`);
8738
+ lines.push(` Sessions with learnings: ${t.totalSessionsWithLearnings}`);
8739
+ if (t.totalSessionsWithoutLearnings > 0) {
8740
+ const rateWithout = t.totalSessionsWithoutLearnings > 0 ? (t.totalFailuresWithoutLearnings / t.totalSessionsWithoutLearnings).toFixed(1) : "0.0";
8741
+ lines.push(` Failure rate (no learnings): ${rateWithout}/session`);
8742
+ }
8743
+ if (t.totalSessionsWithLearnings > 0) {
8744
+ const rateWith = (t.totalFailuresWithLearnings / t.totalSessionsWithLearnings).toFixed(1);
8745
+ lines.push(` Failure rate (with learnings): ${rateWith}/session`);
8746
+ }
8747
+ if (t.totalWasteTokens > 0) {
8748
+ lines.push(` Total waste captured: ${t.totalWasteTokens.toLocaleString()} tokens`);
8749
+ }
8750
+ if (t.estimatedSavingsTokens > 0) {
8751
+ lines.push(` Estimated savings: ~${t.estimatedSavingsTokens.toLocaleString()} tokens`);
8752
+ }
8753
+ return lines.join("\n");
8754
+ }
8755
+
8756
+ // src/commands/learn.ts
8561
8757
  var MIN_EVENTS_FOR_ANALYSIS = 25;
8562
8758
  async function learnObserveCommand(options) {
8563
8759
  try {
@@ -8636,18 +8832,82 @@ async function learnFinalizeCommand(options) {
8636
8832
  existingSkills
8637
8833
  );
8638
8834
  analyzed = true;
8835
+ const waste = calculateSessionWaste(events);
8836
+ const existingLearnedItems = existingLearnedSection ? existingLearnedSection.split("\n").filter((l) => l.startsWith("- ")).length : 0;
8837
+ const hadLearnings = existingLearnedItems > 0;
8838
+ let newLearningsProduced = 0;
8839
+ let roiLearningEntries = [];
8639
8840
  if (response.claudeMdLearnedSection || response.skills?.length) {
8640
8841
  const result = writeLearnedContent({
8641
8842
  claudeMdLearnedSection: response.claudeMdLearnedSection,
8642
8843
  skills: response.skills
8643
8844
  });
8845
+ newLearningsProduced = result.newItemCount;
8644
8846
  if (result.newItemCount > 0) {
8645
- console.log(chalk18.dim(`caliber: learned ${result.newItemCount} new pattern${result.newItemCount === 1 ? "" : "s"}`));
8847
+ const wasteLabel = waste.totalWasteTokens > 0 ? ` (~${waste.totalWasteTokens.toLocaleString()} wasted tokens captured)` : "";
8848
+ console.log(chalk18.dim(`caliber: learned ${result.newItemCount} new pattern${result.newItemCount === 1 ? "" : "s"}${wasteLabel}`));
8646
8849
  for (const item of result.newItems) {
8647
8850
  console.log(chalk18.dim(` + ${item.replace(/^- /, "").slice(0, 80)}`));
8648
8851
  }
8852
+ const wastePerLearning = Math.round(waste.totalWasteTokens / result.newItemCount);
8853
+ const TYPE_RE = /^\*\*\[([^\]]+)\]\*\*/;
8854
+ const learningEntries = result.newItems.map((item) => {
8855
+ const clean = item.replace(/^- /, "");
8856
+ const typeMatch = clean.match(TYPE_RE);
8857
+ return {
8858
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8859
+ observationType: typeMatch ? typeMatch[1] : "unknown",
8860
+ summary: clean.replace(TYPE_RE, "").trim().slice(0, 80),
8861
+ wasteTokens: wastePerLearning,
8862
+ sourceEventCount: events.length
8863
+ };
8864
+ });
8865
+ for (const entry of learningEntries) {
8866
+ trackLearnNewLearning({
8867
+ observationType: entry.observationType,
8868
+ wasteTokens: entry.wasteTokens,
8869
+ sourceEventCount: entry.sourceEventCount
8870
+ });
8871
+ }
8872
+ roiLearningEntries = learningEntries;
8649
8873
  }
8650
8874
  }
8875
+ const sessionSummary = {
8876
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8877
+ sessionId: readState2().sessionId || "unknown",
8878
+ eventCount: events.length,
8879
+ failureCount: waste.failureCount,
8880
+ promptCount: waste.promptCount,
8881
+ hadLearningsAvailable: hadLearnings,
8882
+ learningsCount: existingLearnedItems,
8883
+ newLearningsProduced
8884
+ };
8885
+ const roiStats = recordSession(sessionSummary, roiLearningEntries);
8886
+ trackLearnSessionAnalyzed({
8887
+ eventCount: events.length,
8888
+ failureCount: waste.failureCount,
8889
+ correctionCount: waste.promptCount,
8890
+ hadLearningsAvailable: hadLearnings,
8891
+ learningsAvailableCount: existingLearnedItems,
8892
+ newLearningsProduced,
8893
+ wasteTokens: waste.totalWasteTokens
8894
+ });
8895
+ const t = roiStats.totals;
8896
+ const totalSessions = t.totalSessionsWithLearnings + t.totalSessionsWithoutLearnings;
8897
+ trackLearnROISnapshot({
8898
+ totalWasteTokens: t.totalWasteTokens,
8899
+ totalSessions,
8900
+ sessionsWithLearnings: t.totalSessionsWithLearnings,
8901
+ sessionsWithoutLearnings: t.totalSessionsWithoutLearnings,
8902
+ failureRateWithLearnings: t.totalSessionsWithLearnings > 0 ? t.totalFailuresWithLearnings / t.totalSessionsWithLearnings : 0,
8903
+ failureRateWithoutLearnings: t.totalSessionsWithoutLearnings > 0 ? t.totalFailuresWithoutLearnings / t.totalSessionsWithoutLearnings : 0,
8904
+ estimatedSavingsTokens: t.estimatedSavingsTokens,
8905
+ learningCount: roiStats.learnings.length
8906
+ });
8907
+ if (t.estimatedSavingsTokens > 0) {
8908
+ const totalLearnings = existingLearnedItems + newLearningsProduced;
8909
+ console.log(chalk18.dim(`caliber: ${totalLearnings} learnings active \u2014 est. ~${t.estimatedSavingsTokens.toLocaleString()} tokens saved across ${t.totalSessionsWithLearnings} sessions`));
8910
+ }
8651
8911
  } catch (err) {
8652
8912
  if (options?.force) {
8653
8913
  console.error(chalk18.red("caliber: finalize failed \u2014"), err instanceof Error ? err.message : err);
@@ -8662,7 +8922,7 @@ async function learnFinalizeCommand(options) {
8662
8922
  }
8663
8923
  async function learnInstallCommand() {
8664
8924
  let anyInstalled = false;
8665
- if (fs32.existsSync(".claude")) {
8925
+ if (fs33.existsSync(".claude")) {
8666
8926
  const r = installLearningHooks();
8667
8927
  if (r.installed) {
8668
8928
  console.log(chalk18.green("\u2713") + " Claude Code learning hooks installed");
@@ -8671,7 +8931,7 @@ async function learnInstallCommand() {
8671
8931
  console.log(chalk18.dim(" Claude Code hooks already installed"));
8672
8932
  }
8673
8933
  }
8674
- if (fs32.existsSync(".cursor")) {
8934
+ if (fs33.existsSync(".cursor")) {
8675
8935
  const r = installCursorLearningHooks();
8676
8936
  if (r.installed) {
8677
8937
  console.log(chalk18.green("\u2713") + " Cursor learning hooks installed");
@@ -8680,7 +8940,7 @@ async function learnInstallCommand() {
8680
8940
  console.log(chalk18.dim(" Cursor hooks already installed"));
8681
8941
  }
8682
8942
  }
8683
- if (!fs32.existsSync(".claude") && !fs32.existsSync(".cursor")) {
8943
+ if (!fs33.existsSync(".claude") && !fs33.existsSync(".cursor")) {
8684
8944
  console.log(chalk18.yellow("No .claude/ or .cursor/ directory found."));
8685
8945
  console.log(chalk18.dim(" Run `caliber init` first, or create the directory manually."));
8686
8946
  return;
@@ -8740,12 +9000,21 @@ async function learnStatusCommand() {
8740
9000
  console.log(`
8741
9001
  Learned items in CALIBER_LEARNINGS.md: ${chalk18.cyan(String(lineCount))}`);
8742
9002
  }
9003
+ const roiStats = readROIStats();
9004
+ const roiSummary = formatROISummary(roiStats);
9005
+ if (roiSummary) {
9006
+ console.log();
9007
+ console.log(chalk18.bold(roiSummary.split("\n")[0]));
9008
+ for (const line of roiSummary.split("\n").slice(1)) {
9009
+ console.log(line);
9010
+ }
9011
+ }
8743
9012
  }
8744
9013
 
8745
9014
  // src/cli.ts
8746
- var __dirname = path26.dirname(fileURLToPath(import.meta.url));
9015
+ var __dirname = path27.dirname(fileURLToPath(import.meta.url));
8747
9016
  var pkg = JSON.parse(
8748
- fs33.readFileSync(path26.resolve(__dirname, "..", "package.json"), "utf-8")
9017
+ fs34.readFileSync(path27.resolve(__dirname, "..", "package.json"), "utf-8")
8749
9018
  );
8750
9019
  var program = new Command();
8751
9020
  var displayVersion = process.env.CALIBER_LOCAL ? `${pkg.version}-local` : pkg.version;
@@ -8819,16 +9088,16 @@ learn.command("remove").description("Remove learning hooks from .claude/settings
8819
9088
  learn.command("status").description("Show learning system status").action(tracked("learn:status", learnStatusCommand));
8820
9089
 
8821
9090
  // src/utils/version-check.ts
8822
- import fs34 from "fs";
8823
- import path27 from "path";
9091
+ import fs35 from "fs";
9092
+ import path28 from "path";
8824
9093
  import { fileURLToPath as fileURLToPath2 } from "url";
8825
9094
  import { execSync as execSync14 } from "child_process";
8826
9095
  import chalk19 from "chalk";
8827
9096
  import ora6 from "ora";
8828
9097
  import confirm2 from "@inquirer/confirm";
8829
- var __dirname_vc = path27.dirname(fileURLToPath2(import.meta.url));
9098
+ var __dirname_vc = path28.dirname(fileURLToPath2(import.meta.url));
8830
9099
  var pkg2 = JSON.parse(
8831
- fs34.readFileSync(path27.resolve(__dirname_vc, "..", "package.json"), "utf-8")
9100
+ fs35.readFileSync(path28.resolve(__dirname_vc, "..", "package.json"), "utf-8")
8832
9101
  );
8833
9102
  function getChannel(version) {
8834
9103
  const match = version.match(/-(dev|next)\./);
@@ -8853,8 +9122,8 @@ function isNewer(registry, current) {
8853
9122
  function getInstalledVersion() {
8854
9123
  try {
8855
9124
  const globalRoot = execSync14("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8856
- const pkgPath = path27.join(globalRoot, "@rely-ai", "caliber", "package.json");
8857
- return JSON.parse(fs34.readFileSync(pkgPath, "utf-8")).version;
9125
+ const pkgPath = path28.join(globalRoot, "@rely-ai", "caliber", "package.json");
9126
+ return JSON.parse(fs35.readFileSync(pkgPath, "utf-8")).version;
8858
9127
  } catch {
8859
9128
  return null;
8860
9129
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rely-ai/caliber",
3
- "version": "1.20.0-dev.1773687255",
3
+ "version": "1.20.0-dev.1773690658",
4
4
  "description": "Analyze your codebase and generate optimized AI agent configs (CLAUDE.md, .cursorrules, skills) — no API key needed",
5
5
  "type": "module",
6
6
  "bin": {