@rely-ai/caliber 1.18.9 → 1.19.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.
Files changed (2) hide show
  1. package/dist/bin.js +372 -132
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -177,13 +177,13 @@ __export(lock_exports, {
177
177
  isCaliberRunning: () => isCaliberRunning,
178
178
  releaseLock: () => releaseLock
179
179
  });
180
- import fs27 from "fs";
181
- import path21 from "path";
180
+ import fs28 from "fs";
181
+ import path22 from "path";
182
182
  import os4 from "os";
183
183
  function isCaliberRunning() {
184
184
  try {
185
- if (!fs27.existsSync(LOCK_FILE)) return false;
186
- const raw = fs27.readFileSync(LOCK_FILE, "utf-8").trim();
185
+ if (!fs28.existsSync(LOCK_FILE)) return false;
186
+ const raw = fs28.readFileSync(LOCK_FILE, "utf-8").trim();
187
187
  const { pid, ts } = JSON.parse(raw);
188
188
  if (Date.now() - ts > STALE_MS) return false;
189
189
  try {
@@ -198,13 +198,13 @@ function isCaliberRunning() {
198
198
  }
199
199
  function acquireLock() {
200
200
  try {
201
- fs27.writeFileSync(LOCK_FILE, JSON.stringify({ pid: process.pid, ts: Date.now() }));
201
+ fs28.writeFileSync(LOCK_FILE, JSON.stringify({ pid: process.pid, ts: Date.now() }));
202
202
  } catch {
203
203
  }
204
204
  }
205
205
  function releaseLock() {
206
206
  try {
207
- if (fs27.existsSync(LOCK_FILE)) fs27.unlinkSync(LOCK_FILE);
207
+ if (fs28.existsSync(LOCK_FILE)) fs28.unlinkSync(LOCK_FILE);
208
208
  } catch {
209
209
  }
210
210
  }
@@ -212,14 +212,14 @@ var LOCK_FILE, STALE_MS;
212
212
  var init_lock = __esm({
213
213
  "src/lib/lock.ts"() {
214
214
  "use strict";
215
- LOCK_FILE = path21.join(os4.tmpdir(), ".caliber.lock");
215
+ LOCK_FILE = path22.join(os4.tmpdir(), ".caliber.lock");
216
216
  STALE_MS = 10 * 60 * 1e3;
217
217
  }
218
218
  });
219
219
 
220
220
  // src/cli.ts
221
221
  import { Command } from "commander";
222
- import fs31 from "fs";
222
+ import fs32 from "fs";
223
223
  import path25 from "path";
224
224
  import { fileURLToPath } from "url";
225
225
 
@@ -2030,6 +2030,8 @@ Rules:
2030
2030
  - Be conservative \u2014 don't rewrite sections that aren't affected by the changes
2031
2031
  - Don't add speculative or aspirational content
2032
2032
  - Keep managed blocks (<!-- caliber:managed --> ... <!-- /caliber:managed -->) intact
2033
+ - Do NOT modify CALIBER_LEARNINGS.md \u2014 it is managed separately by the learning system
2034
+ - Preserve any references to CALIBER_LEARNINGS.md in CLAUDE.md
2033
2035
  - If a doc doesn't need updating, return null for it
2034
2036
  - For CLAUDE.md: update commands, architecture notes, conventions, key files if the diffs affect them. Keep under 150 lines.
2035
2037
  - For README.md: update setup instructions, API docs, or feature descriptions if affected
@@ -2061,20 +2063,25 @@ Your job is to reason deeply about these events and identify:
2061
2063
  3. **Workarounds**: When the agent had to abandon one approach entirely and use a different strategy
2062
2064
  4. **Repeated struggles**: The same tool being called many times against the same target, indicating confusion or trial-and-error
2063
2065
  5. **Project-specific conventions**: Commands, paths, patterns, or configurations that are specific to this project and would help future sessions
2066
+ 6. **Anti-patterns**: Commands, approaches, or configurations that consistently fail or cause problems \u2014 things future sessions should explicitly avoid
2064
2067
 
2065
2068
  From these observations, produce:
2066
2069
 
2067
2070
  ### claudeMdLearnedSection
2068
- A markdown section with concise, actionable bullet points that should be added to the project's primary instructions file (CLAUDE.md for Claude Code, AGENTS.md for Codex). Each bullet should be a concrete instruction that prevents a past mistake or encodes a discovered convention. Examples:
2071
+ A markdown section with concise, actionable bullet points. Your output will be written to CALIBER_LEARNINGS.md \u2014 a standalone file that all AI coding agents (Claude Code, Cursor, Codex) reference for project-specific patterns and anti-patterns. Each bullet should be a concrete instruction that prevents a past mistake or encodes a discovered convention. Examples:
2069
2072
  - "Always run \`npm install\` before \`npm run build\` in this project"
2070
2073
  - "The test database requires \`DATABASE_URL\` to be set \u2014 use \`source .env.test\` first"
2071
2074
  - "TypeScript strict mode is enabled \u2014 never use \`any\`, use \`unknown\` with type guards"
2072
2075
  - "Use \`pnpm\` not \`npm\` \u2014 the lockfile is pnpm-lock.yaml"
2076
+ - "Never use \`npm\` in this project \u2014 pnpm-lock.yaml is the lockfile"
2077
+ - "Do NOT run \`jest\` directly \u2014 always use \`npm run test\` which sets the correct env"
2078
+ - "Avoid modifying files in \`src/generated/\` \u2014 they are auto-generated by the build step"
2073
2079
 
2074
2080
  Rules for the learned section:
2075
2081
  - Be additive: keep all existing learned items, add new ones, remove duplicates
2076
2082
  - Never repeat instructions already present in the main CLAUDE.md
2077
2083
  - Each bullet must be specific and actionable \u2014 no vague advice
2084
+ - Include both positive directives ('Always do X') and negative rules ('Never do Y because Z') when the session evidence supports them
2078
2085
  - Maximum ~30 bullet items total
2079
2086
  - Group related items under subheadings if there are many
2080
2087
  - If there's nothing meaningful to learn, return null
@@ -3593,7 +3600,8 @@ function getPrecommitBlock() {
3593
3600
  if [ -x "${bin}" ] || command -v "${bin}" >/dev/null 2>&1; then
3594
3601
  echo "\\033[2mcaliber: refreshing docs...\\033[0m"
3595
3602
  "${bin}" refresh 2>/dev/null || true
3596
- git diff --name-only -- CLAUDE.md .claude/ .cursor/ AGENTS.md 2>/dev/null | xargs git add 2>/dev/null || true
3603
+ "${bin}" learn finalize 2>/dev/null || true
3604
+ git diff --name-only -- CLAUDE.md .claude/ .cursor/ AGENTS.md CALIBER_LEARNINGS.md 2>/dev/null | xargs git add 2>/dev/null || true
3597
3605
  fi
3598
3606
  ${PRECOMMIT_END}`;
3599
3607
  }
@@ -3717,6 +3725,69 @@ function installLearningHooks() {
3717
3725
  writeSettings2(settings);
3718
3726
  return { installed: true, alreadyInstalled: false };
3719
3727
  }
3728
+ var CURSOR_HOOKS_PATH = path13.join(".cursor", "hooks.json");
3729
+ var CURSOR_HOOK_EVENTS = [
3730
+ { event: "postToolUse", tail: "learn observe" },
3731
+ { event: "postToolUseFailure", tail: "learn observe --failure" },
3732
+ { event: "sessionEnd", tail: "learn finalize" }
3733
+ ];
3734
+ function readCursorHooks() {
3735
+ if (!fs18.existsSync(CURSOR_HOOKS_PATH)) return { version: 1, hooks: {} };
3736
+ try {
3737
+ return JSON.parse(fs18.readFileSync(CURSOR_HOOKS_PATH, "utf-8"));
3738
+ } catch {
3739
+ return { version: 1, hooks: {} };
3740
+ }
3741
+ }
3742
+ function writeCursorHooks(config) {
3743
+ const dir = path13.dirname(CURSOR_HOOKS_PATH);
3744
+ if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
3745
+ fs18.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(config, null, 2));
3746
+ }
3747
+ function hasCursorHook(entries, tail) {
3748
+ return entries.some((e) => isCaliberCommand(e.command, tail));
3749
+ }
3750
+ function areCursorLearningHooksInstalled() {
3751
+ const config = readCursorHooks();
3752
+ return CURSOR_HOOK_EVENTS.every((cfg) => {
3753
+ const entries = config.hooks[cfg.event];
3754
+ return Array.isArray(entries) && hasCursorHook(entries, cfg.tail);
3755
+ });
3756
+ }
3757
+ function installCursorLearningHooks() {
3758
+ if (areCursorLearningHooksInstalled()) {
3759
+ return { installed: false, alreadyInstalled: true };
3760
+ }
3761
+ const config = readCursorHooks();
3762
+ const bin = resolveCaliber();
3763
+ for (const cfg of CURSOR_HOOK_EVENTS) {
3764
+ if (!Array.isArray(config.hooks[cfg.event])) {
3765
+ config.hooks[cfg.event] = [];
3766
+ }
3767
+ if (!hasCursorHook(config.hooks[cfg.event], cfg.tail)) {
3768
+ config.hooks[cfg.event].push({ command: `${bin} ${cfg.tail}` });
3769
+ }
3770
+ }
3771
+ writeCursorHooks(config);
3772
+ return { installed: true, alreadyInstalled: false };
3773
+ }
3774
+ function removeCursorLearningHooks() {
3775
+ const config = readCursorHooks();
3776
+ let removedAny = false;
3777
+ for (const cfg of CURSOR_HOOK_EVENTS) {
3778
+ const entries = config.hooks[cfg.event];
3779
+ if (!Array.isArray(entries)) continue;
3780
+ const idx = entries.findIndex((e) => isCaliberCommand(e.command, cfg.tail));
3781
+ if (idx !== -1) {
3782
+ entries.splice(idx, 1);
3783
+ removedAny = true;
3784
+ if (entries.length === 0) delete config.hooks[cfg.event];
3785
+ }
3786
+ }
3787
+ if (!removedAny) return { removed: false, notFound: true };
3788
+ writeCursorHooks(config);
3789
+ return { removed: true, notFound: false };
3790
+ }
3720
3791
  function removeLearningHooks() {
3721
3792
  const settings = readSettings2();
3722
3793
  if (!settings.hooks) return { removed: false, notFound: true };
@@ -3998,6 +4069,7 @@ var POINTS_PERMISSIONS = 2;
3998
4069
  var POINTS_HOOKS = 2;
3999
4070
  var POINTS_AGENTS_MD = 1;
4000
4071
  var POINTS_OPEN_SKILLS_FORMAT = 2;
4072
+ var POINTS_LEARNED_CONTENT = 2;
4001
4073
  var TOKEN_BUDGET_THRESHOLDS = [
4002
4074
  { maxTokens: 2e3, points: 6 },
4003
4075
  { maxTokens: 3500, points: 5 },
@@ -5106,6 +5178,18 @@ function checkBonus(dir) {
5106
5178
  instruction: "Migrate flat skill files to .claude/skills/{name}/SKILL.md with YAML frontmatter."
5107
5179
  } : void 0
5108
5180
  });
5181
+ const learningsContent = readFileOrNull2(join7(dir, "CALIBER_LEARNINGS.md"));
5182
+ const hasLearned = learningsContent ? learningsContent.split("\n").filter((l) => l.startsWith("- ")).length > 0 : false;
5183
+ checks.push({
5184
+ id: "learned_content",
5185
+ name: "Learned content present",
5186
+ category: "bonus",
5187
+ maxPoints: POINTS_LEARNED_CONTENT,
5188
+ earnedPoints: hasLearned ? POINTS_LEARNED_CONTENT : 0,
5189
+ passed: hasLearned,
5190
+ detail: hasLearned ? "Session learnings found in CALIBER_LEARNINGS.md" : "No learned content",
5191
+ suggestion: hasLearned ? void 0 : "Install learning hooks: `caliber learn install`"
5192
+ });
5109
5193
  return checks;
5110
5194
  }
5111
5195
 
@@ -7314,8 +7398,8 @@ async function scoreCommand(options) {
7314
7398
  }
7315
7399
 
7316
7400
  // src/commands/refresh.ts
7317
- import fs28 from "fs";
7318
- import path22 from "path";
7401
+ import fs29 from "fs";
7402
+ import path23 from "path";
7319
7403
  import chalk14 from "chalk";
7320
7404
  import ora5 from "ora";
7321
7405
 
@@ -7327,7 +7411,8 @@ var DOC_PATTERNS = [
7327
7411
  "README.md",
7328
7412
  ".cursorrules",
7329
7413
  ".cursor/rules/",
7330
- ".claude/skills/"
7414
+ ".claude/skills/",
7415
+ "CALIBER_LEARNINGS.md"
7331
7416
  ];
7332
7417
  function excludeArgs() {
7333
7418
  return DOC_PATTERNS.flatMap((p) => ["--", `:!${p}`]);
@@ -7431,8 +7516,8 @@ function writeRefreshDocs(docs) {
7431
7516
 
7432
7517
  // src/ai/refresh.ts
7433
7518
  init_config();
7434
- async function refreshDocs(diff, existingDocs, projectContext) {
7435
- const prompt = buildRefreshPrompt(diff, existingDocs, projectContext);
7519
+ async function refreshDocs(diff, existingDocs, projectContext, learnedSection) {
7520
+ const prompt = buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection);
7436
7521
  const fastModel = getFastModel();
7437
7522
  const raw = await llmCall({
7438
7523
  system: REFRESH_SYSTEM_PROMPT,
@@ -7442,7 +7527,7 @@ async function refreshDocs(diff, existingDocs, projectContext) {
7442
7527
  });
7443
7528
  return parseJsonResponse(raw);
7444
7529
  }
7445
- function buildRefreshPrompt(diff, existingDocs, projectContext) {
7530
+ function buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection) {
7446
7531
  const parts = [];
7447
7532
  parts.push("Update documentation based on the following code changes.\n");
7448
7533
  if (projectContext.packageName) parts.push(`Project: ${projectContext.packageName}`);
@@ -7490,9 +7575,144 @@ Changed files: ${diff.changedFiles.join(", ")}`);
7490
7575
  parts.push(rule.content);
7491
7576
  }
7492
7577
  }
7578
+ if (learnedSection) {
7579
+ parts.push("\n--- Learned Patterns (from session learning) ---");
7580
+ parts.push("Consider these accumulated learnings when deciding what to update:");
7581
+ parts.push(learnedSection);
7582
+ }
7493
7583
  return parts.join("\n");
7494
7584
  }
7495
7585
 
7586
+ // src/learner/writer.ts
7587
+ import fs27 from "fs";
7588
+ import path21 from "path";
7589
+ var LEARNINGS_FILE = "CALIBER_LEARNINGS.md";
7590
+ var LEARNINGS_HEADER = `# Caliber Learnings
7591
+
7592
+ Accumulated patterns and anti-patterns from development sessions.
7593
+ Auto-managed by [caliber](https://github.com/rely-ai-org/caliber) \u2014 do not edit manually.
7594
+
7595
+ `;
7596
+ var LEARNED_START = "<!-- caliber:learned -->";
7597
+ var LEARNED_END = "<!-- /caliber:learned -->";
7598
+ var MAX_LEARNED_ITEMS = 30;
7599
+ function writeLearnedContent(update) {
7600
+ const written = [];
7601
+ let newItemCount = 0;
7602
+ let newItems = [];
7603
+ if (update.claudeMdLearnedSection) {
7604
+ const result = writeLearnedSection(update.claudeMdLearnedSection);
7605
+ newItemCount = result.newCount;
7606
+ newItems = result.newItems;
7607
+ written.push(LEARNINGS_FILE);
7608
+ }
7609
+ if (update.skills?.length) {
7610
+ for (const skill of update.skills) {
7611
+ const skillPath = writeLearnedSkill(skill);
7612
+ written.push(skillPath);
7613
+ }
7614
+ }
7615
+ return { written, newItemCount, newItems };
7616
+ }
7617
+ function parseBullets(content) {
7618
+ const lines = content.split("\n");
7619
+ const bullets = [];
7620
+ let current = "";
7621
+ for (const line of lines) {
7622
+ if (line.startsWith("- ")) {
7623
+ if (current) bullets.push(current);
7624
+ current = line;
7625
+ } else if (current && line.trim() && !line.startsWith("#")) {
7626
+ current += "\n" + line;
7627
+ } else {
7628
+ if (current) bullets.push(current);
7629
+ current = "";
7630
+ }
7631
+ }
7632
+ if (current) bullets.push(current);
7633
+ return bullets;
7634
+ }
7635
+ function normalizeBullet(bullet) {
7636
+ return bullet.replace(/^- /, "").replace(/`[^`]*`/g, "").replace(/\s+/g, " ").toLowerCase().trim();
7637
+ }
7638
+ function deduplicateLearnedItems(existing, incoming) {
7639
+ const existingBullets = existing ? parseBullets(existing) : [];
7640
+ const incomingBullets = parseBullets(incoming);
7641
+ const merged = [...existingBullets];
7642
+ const newItems = [];
7643
+ for (const bullet of incomingBullets) {
7644
+ const norm = normalizeBullet(bullet);
7645
+ if (!norm) continue;
7646
+ const isDup = merged.some((e) => {
7647
+ const eNorm = normalizeBullet(e);
7648
+ const shorter = Math.min(norm.length, eNorm.length);
7649
+ const longer = Math.max(norm.length, eNorm.length);
7650
+ if (!(eNorm.includes(norm) || norm.includes(eNorm))) return false;
7651
+ return shorter / longer > 0.7;
7652
+ });
7653
+ if (!isDup) {
7654
+ merged.push(bullet);
7655
+ newItems.push(bullet);
7656
+ }
7657
+ }
7658
+ const capped = merged.length > MAX_LEARNED_ITEMS ? merged.slice(-MAX_LEARNED_ITEMS) : merged;
7659
+ return { merged: capped.join("\n"), newCount: newItems.length, newItems };
7660
+ }
7661
+ function writeLearnedSection(content) {
7662
+ const existingSection = readLearnedSection();
7663
+ const { merged, newCount, newItems } = deduplicateLearnedItems(existingSection, content);
7664
+ fs27.writeFileSync(LEARNINGS_FILE, LEARNINGS_HEADER + merged + "\n");
7665
+ return { newCount, newItems };
7666
+ }
7667
+ function writeLearnedSkill(skill) {
7668
+ const skillDir = path21.join(".claude", "skills", skill.name);
7669
+ if (!fs27.existsSync(skillDir)) fs27.mkdirSync(skillDir, { recursive: true });
7670
+ const skillPath = path21.join(skillDir, "SKILL.md");
7671
+ if (!skill.isNew && fs27.existsSync(skillPath)) {
7672
+ const existing = fs27.readFileSync(skillPath, "utf-8");
7673
+ fs27.writeFileSync(skillPath, existing.trimEnd() + "\n\n" + skill.content);
7674
+ } else {
7675
+ const frontmatter = [
7676
+ "---",
7677
+ `name: ${skill.name}`,
7678
+ `description: ${skill.description}`,
7679
+ "---",
7680
+ ""
7681
+ ].join("\n");
7682
+ fs27.writeFileSync(skillPath, frontmatter + skill.content);
7683
+ }
7684
+ return skillPath;
7685
+ }
7686
+ function readLearnedSection() {
7687
+ if (fs27.existsSync(LEARNINGS_FILE)) {
7688
+ const content2 = fs27.readFileSync(LEARNINGS_FILE, "utf-8");
7689
+ const bullets = content2.split("\n").filter((l) => l.startsWith("- ")).join("\n");
7690
+ return bullets || null;
7691
+ }
7692
+ const claudeMdPath = "CLAUDE.md";
7693
+ if (!fs27.existsSync(claudeMdPath)) return null;
7694
+ const content = fs27.readFileSync(claudeMdPath, "utf-8");
7695
+ const startIdx = content.indexOf(LEARNED_START);
7696
+ const endIdx = content.indexOf(LEARNED_END);
7697
+ if (startIdx === -1 || endIdx === -1) return null;
7698
+ return content.slice(startIdx + LEARNED_START.length, endIdx).trim() || null;
7699
+ }
7700
+ function migrateInlineLearnings() {
7701
+ if (fs27.existsSync(LEARNINGS_FILE)) return false;
7702
+ const claudeMdPath = "CLAUDE.md";
7703
+ if (!fs27.existsSync(claudeMdPath)) return false;
7704
+ const content = fs27.readFileSync(claudeMdPath, "utf-8");
7705
+ const startIdx = content.indexOf(LEARNED_START);
7706
+ const endIdx = content.indexOf(LEARNED_END);
7707
+ if (startIdx === -1 || endIdx === -1) return false;
7708
+ const section = content.slice(startIdx + LEARNED_START.length, endIdx).trim();
7709
+ if (!section) return false;
7710
+ fs27.writeFileSync(LEARNINGS_FILE, LEARNINGS_HEADER + section + "\n");
7711
+ const cleaned = content.slice(0, startIdx) + content.slice(endIdx + LEARNED_END.length);
7712
+ fs27.writeFileSync(claudeMdPath, cleaned.replace(/\n{3,}/g, "\n\n").trim() + "\n");
7713
+ return true;
7714
+ }
7715
+
7496
7716
  // src/commands/refresh.ts
7497
7717
  init_config();
7498
7718
  function log2(quiet, ...args) {
@@ -7501,11 +7721,11 @@ function log2(quiet, ...args) {
7501
7721
  function discoverGitRepos(parentDir) {
7502
7722
  const repos = [];
7503
7723
  try {
7504
- const entries = fs28.readdirSync(parentDir, { withFileTypes: true });
7724
+ const entries = fs29.readdirSync(parentDir, { withFileTypes: true });
7505
7725
  for (const entry of entries) {
7506
7726
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
7507
- const childPath = path22.join(parentDir, entry.name);
7508
- if (fs28.existsSync(path22.join(childPath, ".git"))) {
7727
+ const childPath = path23.join(parentDir, entry.name);
7728
+ if (fs29.existsSync(path23.join(childPath, ".git"))) {
7509
7729
  repos.push(childPath);
7510
7730
  }
7511
7731
  }
@@ -7529,6 +7749,7 @@ async function refreshSingleRepo(repoDir, options) {
7529
7749
  }
7530
7750
  const spinner = quiet ? null : ora5(`${prefix}Analyzing changes...`).start();
7531
7751
  const existingDocs = readExistingConfigs(repoDir);
7752
+ const learnedSection = readLearnedSection();
7532
7753
  const fingerprint = await collectFingerprint(repoDir);
7533
7754
  const projectContext = {
7534
7755
  languages: fingerprint.languages,
@@ -7544,7 +7765,8 @@ async function refreshSingleRepo(repoDir, options) {
7544
7765
  summary: diff.summary
7545
7766
  },
7546
7767
  existingDocs,
7547
- projectContext
7768
+ projectContext,
7769
+ learnedSection
7548
7770
  );
7549
7771
  if (!response.docsUpdated || response.docsUpdated.length === 0) {
7550
7772
  spinner?.succeed(`${prefix}No doc updates needed`);
@@ -7606,7 +7828,7 @@ async function refreshCommand(options) {
7606
7828
  `));
7607
7829
  const originalDir = process.cwd();
7608
7830
  for (const repo of repos) {
7609
- const repoName = path22.basename(repo);
7831
+ const repoName = path23.basename(repo);
7610
7832
  try {
7611
7833
  process.chdir(repo);
7612
7834
  await refreshSingleRepo(repo, { ...options, label: repoName });
@@ -7830,6 +8052,7 @@ async function configCommand() {
7830
8052
  }
7831
8053
 
7832
8054
  // src/commands/learn.ts
8055
+ import fs31 from "fs";
7833
8056
  import chalk17 from "chalk";
7834
8057
 
7835
8058
  // src/learner/stdin.ts
@@ -7861,8 +8084,8 @@ function readStdin() {
7861
8084
 
7862
8085
  // src/learner/storage.ts
7863
8086
  init_constants();
7864
- import fs29 from "fs";
7865
- import path23 from "path";
8087
+ import fs30 from "fs";
8088
+ import path24 from "path";
7866
8089
  var MAX_RESPONSE_LENGTH = 2e3;
7867
8090
  var DEFAULT_STATE = {
7868
8091
  sessionId: null,
@@ -7870,15 +8093,15 @@ var DEFAULT_STATE = {
7870
8093
  lastAnalysisTimestamp: null
7871
8094
  };
7872
8095
  function ensureLearningDir() {
7873
- if (!fs29.existsSync(LEARNING_DIR)) {
7874
- fs29.mkdirSync(LEARNING_DIR, { recursive: true });
8096
+ if (!fs30.existsSync(LEARNING_DIR)) {
8097
+ fs30.mkdirSync(LEARNING_DIR, { recursive: true });
7875
8098
  }
7876
8099
  }
7877
8100
  function sessionFilePath() {
7878
- return path23.join(LEARNING_DIR, LEARNING_SESSION_FILE);
8101
+ return path24.join(LEARNING_DIR, LEARNING_SESSION_FILE);
7879
8102
  }
7880
8103
  function stateFilePath() {
7881
- return path23.join(LEARNING_DIR, LEARNING_STATE_FILE);
8104
+ return path24.join(LEARNING_DIR, LEARNING_STATE_FILE);
7882
8105
  }
7883
8106
  function truncateResponse(response) {
7884
8107
  const str = JSON.stringify(response);
@@ -7889,113 +8112,84 @@ function appendEvent(event) {
7889
8112
  ensureLearningDir();
7890
8113
  const truncated = { ...event, tool_response: truncateResponse(event.tool_response) };
7891
8114
  const filePath = sessionFilePath();
7892
- fs29.appendFileSync(filePath, JSON.stringify(truncated) + "\n");
8115
+ fs30.appendFileSync(filePath, JSON.stringify(truncated) + "\n");
7893
8116
  const count = getEventCount();
7894
8117
  if (count > LEARNING_MAX_EVENTS) {
7895
- const lines = fs29.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
8118
+ const lines = fs30.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
7896
8119
  const kept = lines.slice(lines.length - LEARNING_MAX_EVENTS);
7897
- fs29.writeFileSync(filePath, kept.join("\n") + "\n");
8120
+ fs30.writeFileSync(filePath, kept.join("\n") + "\n");
7898
8121
  }
7899
8122
  }
7900
8123
  function readAllEvents() {
7901
8124
  const filePath = sessionFilePath();
7902
- if (!fs29.existsSync(filePath)) return [];
7903
- const lines = fs29.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
8125
+ if (!fs30.existsSync(filePath)) return [];
8126
+ const lines = fs30.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
7904
8127
  return lines.map((line) => JSON.parse(line));
7905
8128
  }
7906
8129
  function getEventCount() {
7907
8130
  const filePath = sessionFilePath();
7908
- if (!fs29.existsSync(filePath)) return 0;
7909
- const content = fs29.readFileSync(filePath, "utf-8");
8131
+ if (!fs30.existsSync(filePath)) return 0;
8132
+ const content = fs30.readFileSync(filePath, "utf-8");
7910
8133
  return content.split("\n").filter(Boolean).length;
7911
8134
  }
7912
8135
  function clearSession() {
7913
8136
  const filePath = sessionFilePath();
7914
- if (fs29.existsSync(filePath)) fs29.unlinkSync(filePath);
8137
+ if (fs30.existsSync(filePath)) fs30.unlinkSync(filePath);
7915
8138
  }
7916
8139
  function readState2() {
7917
8140
  const filePath = stateFilePath();
7918
- if (!fs29.existsSync(filePath)) return { ...DEFAULT_STATE };
8141
+ if (!fs30.existsSync(filePath)) return { ...DEFAULT_STATE };
7919
8142
  try {
7920
- return JSON.parse(fs29.readFileSync(filePath, "utf-8"));
8143
+ return JSON.parse(fs30.readFileSync(filePath, "utf-8"));
7921
8144
  } catch {
7922
8145
  return { ...DEFAULT_STATE };
7923
8146
  }
7924
8147
  }
7925
8148
  function writeState2(state) {
7926
8149
  ensureLearningDir();
7927
- fs29.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
8150
+ fs30.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
7928
8151
  }
7929
8152
  function resetState() {
7930
8153
  writeState2({ ...DEFAULT_STATE });
7931
8154
  }
7932
-
7933
- // src/learner/writer.ts
7934
- import fs30 from "fs";
7935
- import path24 from "path";
7936
- var LEARNED_START = "<!-- caliber:learned -->";
7937
- var LEARNED_END = "<!-- /caliber:learned -->";
7938
- function writeLearnedContent(update) {
7939
- const written = [];
7940
- if (update.claudeMdLearnedSection) {
7941
- writeLearnedSection(update.claudeMdLearnedSection);
7942
- written.push("CLAUDE.md");
7943
- }
7944
- if (update.skills?.length) {
7945
- for (const skill of update.skills) {
7946
- const skillPath = writeLearnedSkill(skill);
7947
- written.push(skillPath);
8155
+ var LOCK_FILE2 = "finalize.lock";
8156
+ var LOCK_STALE_MS = 5 * 60 * 1e3;
8157
+ function lockFilePath() {
8158
+ return path24.join(LEARNING_DIR, LOCK_FILE2);
8159
+ }
8160
+ function acquireFinalizeLock() {
8161
+ ensureLearningDir();
8162
+ const lockPath = lockFilePath();
8163
+ if (fs30.existsSync(lockPath)) {
8164
+ try {
8165
+ const stat = fs30.statSync(lockPath);
8166
+ if (Date.now() - stat.mtimeMs < LOCK_STALE_MS) {
8167
+ return false;
8168
+ }
8169
+ } catch {
7948
8170
  }
7949
8171
  }
7950
- return written;
7951
- }
7952
- function writeLearnedSection(content) {
7953
- const claudeMdPath = "CLAUDE.md";
7954
- let existing = "";
7955
- if (fs30.existsSync(claudeMdPath)) {
7956
- existing = fs30.readFileSync(claudeMdPath, "utf-8");
7957
- }
7958
- const section = `${LEARNED_START}
7959
- ${content}
7960
- ${LEARNED_END}`;
7961
- const startIdx = existing.indexOf(LEARNED_START);
7962
- const endIdx = existing.indexOf(LEARNED_END);
7963
- let updated;
7964
- if (startIdx !== -1 && endIdx !== -1) {
7965
- updated = existing.slice(0, startIdx) + section + existing.slice(endIdx + LEARNED_END.length);
7966
- } else {
7967
- const separator = existing.endsWith("\n") || existing === "" ? "" : "\n";
7968
- updated = existing + separator + "\n" + section + "\n";
8172
+ try {
8173
+ fs30.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
8174
+ return true;
8175
+ } catch {
8176
+ try {
8177
+ const stat = fs30.statSync(lockPath);
8178
+ if (Date.now() - stat.mtimeMs >= LOCK_STALE_MS) {
8179
+ fs30.writeFileSync(lockPath, String(process.pid));
8180
+ return true;
8181
+ }
8182
+ } catch {
8183
+ }
8184
+ return false;
7969
8185
  }
7970
- fs30.writeFileSync(claudeMdPath, updated);
7971
8186
  }
7972
- function writeLearnedSkill(skill) {
7973
- const skillDir = path24.join(".claude", "skills", skill.name);
7974
- if (!fs30.existsSync(skillDir)) fs30.mkdirSync(skillDir, { recursive: true });
7975
- const skillPath = path24.join(skillDir, "SKILL.md");
7976
- if (!skill.isNew && fs30.existsSync(skillPath)) {
7977
- const existing = fs30.readFileSync(skillPath, "utf-8");
7978
- fs30.writeFileSync(skillPath, existing.trimEnd() + "\n\n" + skill.content);
7979
- } else {
7980
- const frontmatter = [
7981
- "---",
7982
- `name: ${skill.name}`,
7983
- `description: ${skill.description}`,
7984
- "---",
7985
- ""
7986
- ].join("\n");
7987
- fs30.writeFileSync(skillPath, frontmatter + skill.content);
8187
+ function releaseFinalizeLock() {
8188
+ const lockPath = lockFilePath();
8189
+ try {
8190
+ if (fs30.existsSync(lockPath)) fs30.unlinkSync(lockPath);
8191
+ } catch {
7988
8192
  }
7989
- return skillPath;
7990
- }
7991
- function readLearnedSection() {
7992
- const claudeMdPath = "CLAUDE.md";
7993
- if (!fs30.existsSync(claudeMdPath)) return null;
7994
- const content = fs30.readFileSync(claudeMdPath, "utf-8");
7995
- const startIdx = content.indexOf(LEARNED_START);
7996
- const endIdx = content.indexOf(LEARNED_END);
7997
- if (startIdx === -1 || endIdx === -1) return null;
7998
- return content.slice(startIdx + LEARNED_START.length, endIdx).trim();
7999
8193
  }
8000
8194
 
8001
8195
  // src/ai/learn.ts
@@ -8074,6 +8268,7 @@ ${eventsText}`;
8074
8268
 
8075
8269
  // src/commands/learn.ts
8076
8270
  init_config();
8271
+ var MIN_EVENTS_FOR_ANALYSIS = 50;
8077
8272
  async function learnObserveCommand(options) {
8078
8273
  try {
8079
8274
  const raw = await readStdin();
@@ -8081,11 +8276,11 @@ async function learnObserveCommand(options) {
8081
8276
  const hookData = JSON.parse(raw);
8082
8277
  const event = {
8083
8278
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8084
- session_id: hookData.session_id || "unknown",
8279
+ session_id: hookData.session_id || hookData.conversation_id || "unknown",
8085
8280
  hook_event_name: options.failure ? "PostToolUseFailure" : "PostToolUse",
8086
8281
  tool_name: hookData.tool_name || "unknown",
8087
8282
  tool_input: hookData.tool_input || {},
8088
- tool_response: hookData.tool_response || {},
8283
+ tool_response: hookData.tool_response || hookData.tool_output || {},
8089
8284
  tool_use_id: hookData.tool_use_id || "",
8090
8285
  cwd: hookData.cwd || process.cwd()
8091
8286
  };
@@ -8100,6 +8295,8 @@ async function learnObserveCommand(options) {
8100
8295
  async function learnFinalizeCommand() {
8101
8296
  const { isCaliberRunning: isCaliberRunning2 } = await Promise.resolve().then(() => (init_lock(), lock_exports));
8102
8297
  if (isCaliberRunning2()) return;
8298
+ if (!acquireFinalizeLock()) return;
8299
+ let analyzed = false;
8103
8300
  try {
8104
8301
  const config = loadConfig();
8105
8302
  if (!config) {
@@ -8108,12 +8305,9 @@ async function learnFinalizeCommand() {
8108
8305
  return;
8109
8306
  }
8110
8307
  const events = readAllEvents();
8111
- if (!events.length) {
8112
- clearSession();
8113
- resetState();
8114
- return;
8115
- }
8116
- await validateModel();
8308
+ if (events.length < MIN_EVENTS_FOR_ANALYSIS) return;
8309
+ await validateModel({ fast: true });
8310
+ migrateInlineLearnings();
8117
8311
  const existingConfigs = readExistingConfigs(process.cwd());
8118
8312
  const existingLearnedSection = readLearnedSection();
8119
8313
  const existingSkills = existingConfigs.claudeSkills || [];
@@ -8123,51 +8317,97 @@ async function learnFinalizeCommand() {
8123
8317
  existingLearnedSection,
8124
8318
  existingSkills
8125
8319
  );
8320
+ analyzed = true;
8126
8321
  if (response.claudeMdLearnedSection || response.skills?.length) {
8127
- writeLearnedContent({
8322
+ const result = writeLearnedContent({
8128
8323
  claudeMdLearnedSection: response.claudeMdLearnedSection,
8129
8324
  skills: response.skills
8130
8325
  });
8326
+ if (result.newItemCount > 0) {
8327
+ console.log(chalk17.dim(`caliber: learned ${result.newItemCount} new pattern${result.newItemCount === 1 ? "" : "s"}`));
8328
+ for (const item of result.newItems) {
8329
+ console.log(chalk17.dim(` + ${item.replace(/^- /, "").slice(0, 80)}`));
8330
+ }
8331
+ }
8131
8332
  }
8132
8333
  } catch {
8133
8334
  } finally {
8134
- clearSession();
8135
- resetState();
8335
+ if (analyzed) {
8336
+ clearSession();
8337
+ resetState();
8338
+ }
8339
+ releaseFinalizeLock();
8136
8340
  }
8137
8341
  }
8138
8342
  async function learnInstallCommand() {
8139
- const result = installLearningHooks();
8140
- if (result.alreadyInstalled) {
8141
- console.log(chalk17.dim("Learning hooks already installed."));
8343
+ let anyInstalled = false;
8344
+ if (fs31.existsSync(".claude")) {
8345
+ const r = installLearningHooks();
8346
+ if (r.installed) {
8347
+ console.log(chalk17.green("\u2713") + " Claude Code learning hooks installed");
8348
+ anyInstalled = true;
8349
+ } else if (r.alreadyInstalled) {
8350
+ console.log(chalk17.dim(" Claude Code hooks already installed"));
8351
+ }
8352
+ }
8353
+ if (fs31.existsSync(".cursor")) {
8354
+ const r = installCursorLearningHooks();
8355
+ if (r.installed) {
8356
+ console.log(chalk17.green("\u2713") + " Cursor learning hooks installed");
8357
+ anyInstalled = true;
8358
+ } else if (r.alreadyInstalled) {
8359
+ console.log(chalk17.dim(" Cursor hooks already installed"));
8360
+ }
8361
+ }
8362
+ if (!fs31.existsSync(".claude") && !fs31.existsSync(".cursor")) {
8363
+ console.log(chalk17.yellow("No .claude/ or .cursor/ directory found."));
8364
+ console.log(chalk17.dim(" Run `caliber init` first, or create the directory manually."));
8142
8365
  return;
8143
8366
  }
8144
- console.log(chalk17.green("\u2713") + " Learning hooks installed in .claude/settings.json");
8145
- console.log(chalk17.dim(" PostToolUse, PostToolUseFailure, and SessionEnd hooks active."));
8146
- console.log(chalk17.dim(" Session learnings will be written to CLAUDE.md and skills."));
8367
+ if (anyInstalled) {
8368
+ console.log(chalk17.dim(` Tool usage will be recorded and learnings extracted after \u2265${MIN_EVENTS_FOR_ANALYSIS} events.`));
8369
+ console.log(chalk17.dim(" Learnings written to CALIBER_LEARNINGS.md."));
8370
+ }
8147
8371
  }
8148
8372
  async function learnRemoveCommand() {
8149
- const result = removeLearningHooks();
8150
- if (result.notFound) {
8151
- console.log(chalk17.dim("Learning hooks not found."));
8152
- return;
8373
+ let anyRemoved = false;
8374
+ const r1 = removeLearningHooks();
8375
+ if (r1.removed) {
8376
+ console.log(chalk17.green("\u2713") + " Claude Code learning hooks removed");
8377
+ anyRemoved = true;
8378
+ }
8379
+ const r2 = removeCursorLearningHooks();
8380
+ if (r2.removed) {
8381
+ console.log(chalk17.green("\u2713") + " Cursor learning hooks removed");
8382
+ anyRemoved = true;
8383
+ }
8384
+ if (!anyRemoved) {
8385
+ console.log(chalk17.dim("No learning hooks found."));
8153
8386
  }
8154
- console.log(chalk17.green("\u2713") + " Learning hooks removed from .claude/settings.json");
8155
8387
  }
8156
8388
  async function learnStatusCommand() {
8157
- const installed = areLearningHooksInstalled();
8389
+ const claudeInstalled = areLearningHooksInstalled();
8390
+ const cursorInstalled = areCursorLearningHooksInstalled();
8158
8391
  const state = readState2();
8159
8392
  const eventCount = getEventCount();
8160
8393
  console.log(chalk17.bold("Session Learning Status"));
8161
8394
  console.log();
8162
- if (installed) {
8163
- console.log(chalk17.green("\u2713") + " Learning hooks are " + chalk17.green("installed"));
8395
+ if (claudeInstalled) {
8396
+ console.log(chalk17.green("\u2713") + " Claude Code hooks " + chalk17.green("installed"));
8397
+ } else {
8398
+ console.log(chalk17.dim("\u2717") + " Claude Code hooks " + chalk17.dim("not installed"));
8399
+ }
8400
+ if (cursorInstalled) {
8401
+ console.log(chalk17.green("\u2713") + " Cursor hooks " + chalk17.green("installed"));
8164
8402
  } else {
8165
- console.log(chalk17.dim("\u2717") + " Learning hooks are " + chalk17.yellow("not installed"));
8403
+ console.log(chalk17.dim("\u2717") + " Cursor hooks " + chalk17.dim("not installed"));
8404
+ }
8405
+ if (!claudeInstalled && !cursorInstalled) {
8166
8406
  console.log(chalk17.dim(" Run `caliber learn install` to enable session learning."));
8167
8407
  }
8168
8408
  console.log();
8169
8409
  console.log(`Events recorded: ${chalk17.cyan(String(eventCount))}`);
8170
- console.log(`Total this session: ${chalk17.cyan(String(state.eventCount))}`);
8410
+ console.log(`Threshold for analysis: ${chalk17.cyan(String(MIN_EVENTS_FOR_ANALYSIS))}`);
8171
8411
  if (state.lastAnalysisTimestamp) {
8172
8412
  console.log(`Last analysis: ${chalk17.cyan(state.lastAnalysisTimestamp)}`);
8173
8413
  } else {
@@ -8177,14 +8417,14 @@ async function learnStatusCommand() {
8177
8417
  if (learnedSection) {
8178
8418
  const lineCount = learnedSection.split("\n").filter(Boolean).length;
8179
8419
  console.log(`
8180
- Learned items in CLAUDE.md: ${chalk17.cyan(String(lineCount))}`);
8420
+ Learned items in CALIBER_LEARNINGS.md: ${chalk17.cyan(String(lineCount))}`);
8181
8421
  }
8182
8422
  }
8183
8423
 
8184
8424
  // src/cli.ts
8185
8425
  var __dirname = path25.dirname(fileURLToPath(import.meta.url));
8186
8426
  var pkg = JSON.parse(
8187
- fs31.readFileSync(path25.resolve(__dirname, "..", "package.json"), "utf-8")
8427
+ fs32.readFileSync(path25.resolve(__dirname, "..", "package.json"), "utf-8")
8188
8428
  );
8189
8429
  var program = new Command();
8190
8430
  var displayVersion = process.env.CALIBER_LOCAL ? `${pkg.version}-local` : pkg.version;
@@ -8258,7 +8498,7 @@ learn.command("remove").description("Remove learning hooks from .claude/settings
8258
8498
  learn.command("status").description("Show learning system status").action(tracked("learn:status", learnStatusCommand));
8259
8499
 
8260
8500
  // src/utils/version-check.ts
8261
- import fs32 from "fs";
8501
+ import fs33 from "fs";
8262
8502
  import path26 from "path";
8263
8503
  import { fileURLToPath as fileURLToPath2 } from "url";
8264
8504
  import { execSync as execSync14 } from "child_process";
@@ -8267,13 +8507,13 @@ import ora6 from "ora";
8267
8507
  import confirm2 from "@inquirer/confirm";
8268
8508
  var __dirname_vc = path26.dirname(fileURLToPath2(import.meta.url));
8269
8509
  var pkg2 = JSON.parse(
8270
- fs32.readFileSync(path26.resolve(__dirname_vc, "..", "package.json"), "utf-8")
8510
+ fs33.readFileSync(path26.resolve(__dirname_vc, "..", "package.json"), "utf-8")
8271
8511
  );
8272
8512
  function getInstalledVersion() {
8273
8513
  try {
8274
8514
  const globalRoot = execSync14("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8275
8515
  const pkgPath = path26.join(globalRoot, "@rely-ai", "caliber", "package.json");
8276
- return JSON.parse(fs32.readFileSync(pkgPath, "utf-8")).version;
8516
+ return JSON.parse(fs33.readFileSync(pkgPath, "utf-8")).version;
8277
8517
  } catch {
8278
8518
  return null;
8279
8519
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rely-ai/caliber",
3
- "version": "1.18.9",
3
+ "version": "1.19.0",
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": {