@rely-ai/caliber 1.20.0-dev.1773686790 → 1.20.0-dev.1773690306

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 +244 -26
  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);
@@ -5757,6 +5759,36 @@ function trackSkillsInstalled(count) {
5757
5759
  function trackUndoExecuted() {
5758
5760
  trackEvent("undo_executed");
5759
5761
  }
5762
+ function trackLearnSessionAnalyzed(props) {
5763
+ trackEvent("learn_session_analyzed", {
5764
+ event_count: props.eventCount,
5765
+ failure_count: props.failureCount,
5766
+ correction_count: props.correctionCount,
5767
+ had_learnings_available: props.hadLearningsAvailable,
5768
+ learnings_available_count: props.learningsAvailableCount,
5769
+ new_learnings_produced: props.newLearningsProduced,
5770
+ waste_tokens: props.wasteTokens
5771
+ });
5772
+ }
5773
+ function trackLearnROISnapshot(props) {
5774
+ trackEvent("learn_roi_snapshot", {
5775
+ total_waste_tokens: props.totalWasteTokens,
5776
+ total_sessions: props.totalSessions,
5777
+ sessions_with_learnings: props.sessionsWithLearnings,
5778
+ sessions_without_learnings: props.sessionsWithoutLearnings,
5779
+ failure_rate_with_learnings: props.failureRateWithLearnings,
5780
+ failure_rate_without_learnings: props.failureRateWithoutLearnings,
5781
+ estimated_savings_tokens: props.estimatedSavingsTokens,
5782
+ learning_count: props.learningCount
5783
+ });
5784
+ }
5785
+ function trackLearnNewLearning(props) {
5786
+ trackEvent("learn_new_learning", {
5787
+ observation_type: props.observationType,
5788
+ waste_tokens: props.wasteTokens,
5789
+ source_event_count: props.sourceEventCount
5790
+ });
5791
+ }
5760
5792
 
5761
5793
  // src/commands/recommend.ts
5762
5794
  function detectLocalPlatforms() {
@@ -7084,7 +7116,7 @@ async function refineLoop(currentSetup, _targetAgent, sessionHistory) {
7084
7116
  }
7085
7117
  function summarizeSetup(action, setup) {
7086
7118
  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(", ");
7119
+ const files = descriptions ? Object.entries(descriptions).map(([path29, desc]) => ` ${path29}: ${desc}`).join("\n") : Object.keys(setup).filter((k) => k !== "targetAgent" && k !== "fileDescriptions").join(", ");
7088
7120
  return `${action}. Files:
7089
7121
  ${files}`;
7090
7122
  }
@@ -7165,12 +7197,10 @@ async function promptAgent() {
7165
7197
  async function promptHookType(targetAgent) {
7166
7198
  const choices = [];
7167
7199
  const hasClaude = targetAgent.includes("claude");
7200
+ choices.push({ name: "Git pre-commit hook \u2014 refresh before each commit (recommended)", value: "precommit" });
7168
7201
  if (hasClaude) {
7169
- choices.push({ name: "Claude Code hook (auto-refresh on session end)", value: "claude" });
7170
- }
7171
- choices.push({ name: "Git pre-commit hook (refresh before each commit)", value: "precommit" });
7172
- if (hasClaude) {
7173
- choices.push({ name: "Both (Claude Code + pre-commit)", value: "both" });
7202
+ choices.push({ name: "Claude Code hook \u2014 auto-refresh on session end", value: "claude" });
7203
+ choices.push({ name: "Both (pre-commit + Claude Code)", value: "both" });
7174
7204
  }
7175
7205
  choices.push({ name: "Skip for now", value: "skip" });
7176
7206
  return select5({
@@ -8287,7 +8317,7 @@ async function configCommand() {
8287
8317
  }
8288
8318
 
8289
8319
  // src/commands/learn.ts
8290
- import fs32 from "fs";
8320
+ import fs33 from "fs";
8291
8321
  import chalk18 from "chalk";
8292
8322
 
8293
8323
  // src/learner/stdin.ts
@@ -8557,9 +8587,124 @@ ${eventsText}`;
8557
8587
  });
8558
8588
  return parseAnalysisResponse(raw);
8559
8589
  }
8590
+ function calculateSessionWaste(events) {
8591
+ let totalWasteTokens = 0;
8592
+ let failureCount = 0;
8593
+ let promptCount = 0;
8594
+ for (const event of events) {
8595
+ if (event.hook_event_name === "PostToolUseFailure") {
8596
+ const te = event;
8597
+ const inputStr = JSON.stringify(te.tool_input);
8598
+ const responseStr = typeof te.tool_response === "object" && "_truncated" in te.tool_response ? String(te.tool_response._truncated) : JSON.stringify(te.tool_response);
8599
+ totalWasteTokens += estimateTokens(inputStr + responseStr);
8600
+ failureCount++;
8601
+ } else if (event.hook_event_name === "UserPromptSubmit") {
8602
+ promptCount++;
8603
+ }
8604
+ }
8605
+ return { totalWasteTokens, failureCount, promptCount };
8606
+ }
8560
8607
 
8561
8608
  // src/commands/learn.ts
8562
8609
  init_config();
8610
+
8611
+ // src/learner/roi.ts
8612
+ init_constants();
8613
+ import fs32 from "fs";
8614
+ import path26 from "path";
8615
+ var DEFAULT_TOTALS = {
8616
+ totalWasteTokens: 0,
8617
+ totalSessionsWithLearnings: 0,
8618
+ totalSessionsWithoutLearnings: 0,
8619
+ totalFailuresWithLearnings: 0,
8620
+ totalFailuresWithoutLearnings: 0,
8621
+ estimatedSavingsTokens: 0,
8622
+ firstSessionTimestamp: "",
8623
+ lastSessionTimestamp: ""
8624
+ };
8625
+ function roiFilePath() {
8626
+ return path26.join(LEARNING_DIR, LEARNING_ROI_FILE);
8627
+ }
8628
+ function readROIStats() {
8629
+ const filePath = roiFilePath();
8630
+ if (!fs32.existsSync(filePath)) {
8631
+ return { learnings: [], sessions: [], totals: { ...DEFAULT_TOTALS } };
8632
+ }
8633
+ try {
8634
+ return JSON.parse(fs32.readFileSync(filePath, "utf-8"));
8635
+ } catch {
8636
+ return { learnings: [], sessions: [], totals: { ...DEFAULT_TOTALS } };
8637
+ }
8638
+ }
8639
+ function writeROIStats(stats) {
8640
+ ensureLearningDir();
8641
+ fs32.writeFileSync(roiFilePath(), JSON.stringify(stats, null, 2));
8642
+ }
8643
+ function recalculateTotals(stats) {
8644
+ const totals = stats.totals;
8645
+ totals.totalWasteTokens = stats.learnings.reduce((sum, l) => sum + l.wasteTokens, 0);
8646
+ totals.totalSessionsWithLearnings = 0;
8647
+ totals.totalSessionsWithoutLearnings = 0;
8648
+ totals.totalFailuresWithLearnings = 0;
8649
+ totals.totalFailuresWithoutLearnings = 0;
8650
+ for (const s of stats.sessions) {
8651
+ if (s.hadLearningsAvailable) {
8652
+ totals.totalSessionsWithLearnings++;
8653
+ totals.totalFailuresWithLearnings += s.failureCount;
8654
+ } else {
8655
+ totals.totalSessionsWithoutLearnings++;
8656
+ totals.totalFailuresWithoutLearnings += s.failureCount;
8657
+ }
8658
+ }
8659
+ totals.estimatedSavingsTokens = totals.totalWasteTokens * totals.totalSessionsWithLearnings;
8660
+ if (stats.sessions.length > 0) {
8661
+ totals.firstSessionTimestamp = stats.sessions[0].timestamp;
8662
+ totals.lastSessionTimestamp = stats.sessions[stats.sessions.length - 1].timestamp;
8663
+ }
8664
+ }
8665
+ var MAX_SESSIONS = 500;
8666
+ var MAX_LEARNINGS = 1e3;
8667
+ function recordSession(summary, learnings) {
8668
+ const stats = readROIStats();
8669
+ stats.sessions.push(summary);
8670
+ if (learnings?.length) {
8671
+ stats.learnings.push(...learnings);
8672
+ }
8673
+ if (stats.sessions.length > MAX_SESSIONS) {
8674
+ stats.sessions = stats.sessions.slice(-MAX_SESSIONS);
8675
+ }
8676
+ if (stats.learnings.length > MAX_LEARNINGS) {
8677
+ stats.learnings = stats.learnings.slice(-MAX_LEARNINGS);
8678
+ }
8679
+ recalculateTotals(stats);
8680
+ writeROIStats(stats);
8681
+ return stats;
8682
+ }
8683
+ function formatROISummary(stats) {
8684
+ const t = stats.totals;
8685
+ const totalSessions = t.totalSessionsWithLearnings + t.totalSessionsWithoutLearnings;
8686
+ if (totalSessions === 0) return "";
8687
+ const lines = ["ROI Summary"];
8688
+ lines.push(` Sessions tracked: ${totalSessions}`);
8689
+ lines.push(` Sessions with learnings: ${t.totalSessionsWithLearnings}`);
8690
+ if (t.totalSessionsWithoutLearnings > 0) {
8691
+ const rateWithout = t.totalSessionsWithoutLearnings > 0 ? (t.totalFailuresWithoutLearnings / t.totalSessionsWithoutLearnings).toFixed(1) : "0.0";
8692
+ lines.push(` Failure rate (no learnings): ${rateWithout}/session`);
8693
+ }
8694
+ if (t.totalSessionsWithLearnings > 0) {
8695
+ const rateWith = (t.totalFailuresWithLearnings / t.totalSessionsWithLearnings).toFixed(1);
8696
+ lines.push(` Failure rate (with learnings): ${rateWith}/session`);
8697
+ }
8698
+ if (t.totalWasteTokens > 0) {
8699
+ lines.push(` Total waste captured: ${t.totalWasteTokens.toLocaleString()} tokens`);
8700
+ }
8701
+ if (t.estimatedSavingsTokens > 0) {
8702
+ lines.push(` Estimated savings: ~${t.estimatedSavingsTokens.toLocaleString()} tokens`);
8703
+ }
8704
+ return lines.join("\n");
8705
+ }
8706
+
8707
+ // src/commands/learn.ts
8563
8708
  var MIN_EVENTS_FOR_ANALYSIS = 25;
8564
8709
  async function learnObserveCommand(options) {
8565
8710
  try {
@@ -8638,18 +8783,82 @@ async function learnFinalizeCommand(options) {
8638
8783
  existingSkills
8639
8784
  );
8640
8785
  analyzed = true;
8786
+ const waste = calculateSessionWaste(events);
8787
+ const existingLearnedItems = existingLearnedSection ? existingLearnedSection.split("\n").filter((l) => l.startsWith("- ")).length : 0;
8788
+ const hadLearnings = existingLearnedItems > 0;
8789
+ let newLearningsProduced = 0;
8790
+ let roiLearningEntries = [];
8641
8791
  if (response.claudeMdLearnedSection || response.skills?.length) {
8642
8792
  const result = writeLearnedContent({
8643
8793
  claudeMdLearnedSection: response.claudeMdLearnedSection,
8644
8794
  skills: response.skills
8645
8795
  });
8796
+ newLearningsProduced = result.newItemCount;
8646
8797
  if (result.newItemCount > 0) {
8647
- console.log(chalk18.dim(`caliber: learned ${result.newItemCount} new pattern${result.newItemCount === 1 ? "" : "s"}`));
8798
+ const wasteLabel = waste.totalWasteTokens > 0 ? ` (~${waste.totalWasteTokens.toLocaleString()} wasted tokens captured)` : "";
8799
+ console.log(chalk18.dim(`caliber: learned ${result.newItemCount} new pattern${result.newItemCount === 1 ? "" : "s"}${wasteLabel}`));
8648
8800
  for (const item of result.newItems) {
8649
8801
  console.log(chalk18.dim(` + ${item.replace(/^- /, "").slice(0, 80)}`));
8650
8802
  }
8803
+ const wastePerLearning = Math.round(waste.totalWasteTokens / result.newItemCount);
8804
+ const TYPE_RE = /^\*\*\[([^\]]+)\]\*\*/;
8805
+ const learningEntries = result.newItems.map((item) => {
8806
+ const clean = item.replace(/^- /, "");
8807
+ const typeMatch = clean.match(TYPE_RE);
8808
+ return {
8809
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8810
+ observationType: typeMatch ? typeMatch[1] : "unknown",
8811
+ summary: clean.replace(TYPE_RE, "").trim().slice(0, 80),
8812
+ wasteTokens: wastePerLearning,
8813
+ sourceEventCount: events.length
8814
+ };
8815
+ });
8816
+ for (const entry of learningEntries) {
8817
+ trackLearnNewLearning({
8818
+ observationType: entry.observationType,
8819
+ wasteTokens: entry.wasteTokens,
8820
+ sourceEventCount: entry.sourceEventCount
8821
+ });
8822
+ }
8823
+ roiLearningEntries = learningEntries;
8651
8824
  }
8652
8825
  }
8826
+ const sessionSummary = {
8827
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8828
+ sessionId: readState2().sessionId || "unknown",
8829
+ eventCount: events.length,
8830
+ failureCount: waste.failureCount,
8831
+ promptCount: waste.promptCount,
8832
+ hadLearningsAvailable: hadLearnings,
8833
+ learningsCount: existingLearnedItems,
8834
+ newLearningsProduced
8835
+ };
8836
+ const roiStats = recordSession(sessionSummary, roiLearningEntries);
8837
+ trackLearnSessionAnalyzed({
8838
+ eventCount: events.length,
8839
+ failureCount: waste.failureCount,
8840
+ correctionCount: waste.promptCount,
8841
+ hadLearningsAvailable: hadLearnings,
8842
+ learningsAvailableCount: existingLearnedItems,
8843
+ newLearningsProduced,
8844
+ wasteTokens: waste.totalWasteTokens
8845
+ });
8846
+ const t = roiStats.totals;
8847
+ const totalSessions = t.totalSessionsWithLearnings + t.totalSessionsWithoutLearnings;
8848
+ trackLearnROISnapshot({
8849
+ totalWasteTokens: t.totalWasteTokens,
8850
+ totalSessions,
8851
+ sessionsWithLearnings: t.totalSessionsWithLearnings,
8852
+ sessionsWithoutLearnings: t.totalSessionsWithoutLearnings,
8853
+ failureRateWithLearnings: t.totalSessionsWithLearnings > 0 ? t.totalFailuresWithLearnings / t.totalSessionsWithLearnings : 0,
8854
+ failureRateWithoutLearnings: t.totalSessionsWithoutLearnings > 0 ? t.totalFailuresWithoutLearnings / t.totalSessionsWithoutLearnings : 0,
8855
+ estimatedSavingsTokens: t.estimatedSavingsTokens,
8856
+ learningCount: roiStats.learnings.length
8857
+ });
8858
+ if (t.estimatedSavingsTokens > 0) {
8859
+ const totalLearnings = existingLearnedItems + newLearningsProduced;
8860
+ console.log(chalk18.dim(`caliber: ${totalLearnings} learnings active \u2014 est. ~${t.estimatedSavingsTokens.toLocaleString()} tokens saved across ${t.totalSessionsWithLearnings} sessions`));
8861
+ }
8653
8862
  } catch (err) {
8654
8863
  if (options?.force) {
8655
8864
  console.error(chalk18.red("caliber: finalize failed \u2014"), err instanceof Error ? err.message : err);
@@ -8664,7 +8873,7 @@ async function learnFinalizeCommand(options) {
8664
8873
  }
8665
8874
  async function learnInstallCommand() {
8666
8875
  let anyInstalled = false;
8667
- if (fs32.existsSync(".claude")) {
8876
+ if (fs33.existsSync(".claude")) {
8668
8877
  const r = installLearningHooks();
8669
8878
  if (r.installed) {
8670
8879
  console.log(chalk18.green("\u2713") + " Claude Code learning hooks installed");
@@ -8673,7 +8882,7 @@ async function learnInstallCommand() {
8673
8882
  console.log(chalk18.dim(" Claude Code hooks already installed"));
8674
8883
  }
8675
8884
  }
8676
- if (fs32.existsSync(".cursor")) {
8885
+ if (fs33.existsSync(".cursor")) {
8677
8886
  const r = installCursorLearningHooks();
8678
8887
  if (r.installed) {
8679
8888
  console.log(chalk18.green("\u2713") + " Cursor learning hooks installed");
@@ -8682,7 +8891,7 @@ async function learnInstallCommand() {
8682
8891
  console.log(chalk18.dim(" Cursor hooks already installed"));
8683
8892
  }
8684
8893
  }
8685
- if (!fs32.existsSync(".claude") && !fs32.existsSync(".cursor")) {
8894
+ if (!fs33.existsSync(".claude") && !fs33.existsSync(".cursor")) {
8686
8895
  console.log(chalk18.yellow("No .claude/ or .cursor/ directory found."));
8687
8896
  console.log(chalk18.dim(" Run `caliber init` first, or create the directory manually."));
8688
8897
  return;
@@ -8742,12 +8951,21 @@ async function learnStatusCommand() {
8742
8951
  console.log(`
8743
8952
  Learned items in CALIBER_LEARNINGS.md: ${chalk18.cyan(String(lineCount))}`);
8744
8953
  }
8954
+ const roiStats = readROIStats();
8955
+ const roiSummary = formatROISummary(roiStats);
8956
+ if (roiSummary) {
8957
+ console.log();
8958
+ console.log(chalk18.bold(roiSummary.split("\n")[0]));
8959
+ for (const line of roiSummary.split("\n").slice(1)) {
8960
+ console.log(line);
8961
+ }
8962
+ }
8745
8963
  }
8746
8964
 
8747
8965
  // src/cli.ts
8748
- var __dirname = path26.dirname(fileURLToPath(import.meta.url));
8966
+ var __dirname = path27.dirname(fileURLToPath(import.meta.url));
8749
8967
  var pkg = JSON.parse(
8750
- fs33.readFileSync(path26.resolve(__dirname, "..", "package.json"), "utf-8")
8968
+ fs34.readFileSync(path27.resolve(__dirname, "..", "package.json"), "utf-8")
8751
8969
  );
8752
8970
  var program = new Command();
8753
8971
  var displayVersion = process.env.CALIBER_LOCAL ? `${pkg.version}-local` : pkg.version;
@@ -8821,16 +9039,16 @@ learn.command("remove").description("Remove learning hooks from .claude/settings
8821
9039
  learn.command("status").description("Show learning system status").action(tracked("learn:status", learnStatusCommand));
8822
9040
 
8823
9041
  // src/utils/version-check.ts
8824
- import fs34 from "fs";
8825
- import path27 from "path";
9042
+ import fs35 from "fs";
9043
+ import path28 from "path";
8826
9044
  import { fileURLToPath as fileURLToPath2 } from "url";
8827
9045
  import { execSync as execSync14 } from "child_process";
8828
9046
  import chalk19 from "chalk";
8829
9047
  import ora6 from "ora";
8830
9048
  import confirm2 from "@inquirer/confirm";
8831
- var __dirname_vc = path27.dirname(fileURLToPath2(import.meta.url));
9049
+ var __dirname_vc = path28.dirname(fileURLToPath2(import.meta.url));
8832
9050
  var pkg2 = JSON.parse(
8833
- fs34.readFileSync(path27.resolve(__dirname_vc, "..", "package.json"), "utf-8")
9051
+ fs35.readFileSync(path28.resolve(__dirname_vc, "..", "package.json"), "utf-8")
8834
9052
  );
8835
9053
  function getChannel(version) {
8836
9054
  const match = version.match(/-(dev|next)\./);
@@ -8855,8 +9073,8 @@ function isNewer(registry, current) {
8855
9073
  function getInstalledVersion() {
8856
9074
  try {
8857
9075
  const globalRoot = execSync14("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8858
- const pkgPath = path27.join(globalRoot, "@rely-ai", "caliber", "package.json");
8859
- return JSON.parse(fs34.readFileSync(pkgPath, "utf-8")).version;
9076
+ const pkgPath = path28.join(globalRoot, "@rely-ai", "caliber", "package.json");
9077
+ return JSON.parse(fs35.readFileSync(pkgPath, "utf-8")).version;
8860
9078
  } catch {
8861
9079
  return null;
8862
9080
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rely-ai/caliber",
3
- "version": "1.20.0-dev.1773686790",
3
+ "version": "1.20.0-dev.1773690306",
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": {