@rely-ai/caliber 1.18.9 → 1.19.1

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 +393 -148
  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
 
@@ -1382,11 +1382,15 @@ var ClaudeCliProvider = class {
1382
1382
  const combined = this.buildCombinedPrompt(options);
1383
1383
  const args = ["-p"];
1384
1384
  if (options.model) args.push("--model", options.model);
1385
- const child = spawn2(CLAUDE_CLI_BIN, args, {
1385
+ const child = IS_WINDOWS2 ? spawn2([CLAUDE_CLI_BIN, ...args].join(" "), {
1386
1386
  cwd: process.cwd(),
1387
1387
  stdio: ["pipe", "pipe", "inherit"],
1388
1388
  env: process.env,
1389
- ...IS_WINDOWS2 && { shell: true }
1389
+ shell: true
1390
+ }) : spawn2(CLAUDE_CLI_BIN, args, {
1391
+ cwd: process.cwd(),
1392
+ stdio: ["pipe", "pipe", "inherit"],
1393
+ env: process.env
1390
1394
  });
1391
1395
  child.stdin.end(combined);
1392
1396
  let settled = false;
@@ -1446,11 +1450,15 @@ ${msg.content}
1446
1450
  return new Promise((resolve2, reject) => {
1447
1451
  const args = ["-p"];
1448
1452
  if (model) args.push("--model", model);
1449
- const child = spawn2(CLAUDE_CLI_BIN, args, {
1453
+ const child = IS_WINDOWS2 ? spawn2([CLAUDE_CLI_BIN, ...args].join(" "), {
1450
1454
  cwd: process.cwd(),
1451
1455
  stdio: ["pipe", "pipe", "inherit"],
1452
1456
  env: process.env,
1453
- ...IS_WINDOWS2 && { shell: true }
1457
+ shell: true
1458
+ }) : spawn2(CLAUDE_CLI_BIN, args, {
1459
+ cwd: process.cwd(),
1460
+ stdio: ["pipe", "pipe", "inherit"],
1461
+ env: process.env
1454
1462
  });
1455
1463
  child.stdin.end(combinedPrompt);
1456
1464
  const chunks = [];
@@ -2030,6 +2038,8 @@ Rules:
2030
2038
  - Be conservative \u2014 don't rewrite sections that aren't affected by the changes
2031
2039
  - Don't add speculative or aspirational content
2032
2040
  - Keep managed blocks (<!-- caliber:managed --> ... <!-- /caliber:managed -->) intact
2041
+ - Do NOT modify CALIBER_LEARNINGS.md \u2014 it is managed separately by the learning system
2042
+ - Preserve any references to CALIBER_LEARNINGS.md in CLAUDE.md
2033
2043
  - If a doc doesn't need updating, return null for it
2034
2044
  - For CLAUDE.md: update commands, architecture notes, conventions, key files if the diffs affect them. Keep under 150 lines.
2035
2045
  - For README.md: update setup instructions, API docs, or feature descriptions if affected
@@ -2061,20 +2071,25 @@ Your job is to reason deeply about these events and identify:
2061
2071
  3. **Workarounds**: When the agent had to abandon one approach entirely and use a different strategy
2062
2072
  4. **Repeated struggles**: The same tool being called many times against the same target, indicating confusion or trial-and-error
2063
2073
  5. **Project-specific conventions**: Commands, paths, patterns, or configurations that are specific to this project and would help future sessions
2074
+ 6. **Anti-patterns**: Commands, approaches, or configurations that consistently fail or cause problems \u2014 things future sessions should explicitly avoid
2064
2075
 
2065
2076
  From these observations, produce:
2066
2077
 
2067
2078
  ### 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:
2079
+ 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
2080
  - "Always run \`npm install\` before \`npm run build\` in this project"
2070
2081
  - "The test database requires \`DATABASE_URL\` to be set \u2014 use \`source .env.test\` first"
2071
2082
  - "TypeScript strict mode is enabled \u2014 never use \`any\`, use \`unknown\` with type guards"
2072
2083
  - "Use \`pnpm\` not \`npm\` \u2014 the lockfile is pnpm-lock.yaml"
2084
+ - "Never use \`npm\` in this project \u2014 pnpm-lock.yaml is the lockfile"
2085
+ - "Do NOT run \`jest\` directly \u2014 always use \`npm run test\` which sets the correct env"
2086
+ - "Avoid modifying files in \`src/generated/\` \u2014 they are auto-generated by the build step"
2073
2087
 
2074
2088
  Rules for the learned section:
2075
2089
  - Be additive: keep all existing learned items, add new ones, remove duplicates
2076
2090
  - Never repeat instructions already present in the main CLAUDE.md
2077
2091
  - Each bullet must be specific and actionable \u2014 no vague advice
2092
+ - Include both positive directives ('Always do X') and negative rules ('Never do Y because Z') when the session evidence supports them
2078
2093
  - Maximum ~30 bullet items total
2079
2094
  - Group related items under subheadings if there are many
2080
2095
  - If there's nothing meaningful to learn, return null
@@ -3193,18 +3208,13 @@ function openDiffsInEditor(editor, files) {
3193
3208
  const cmd = editor === "cursor" ? "cursor" : "code";
3194
3209
  for (const file of files) {
3195
3210
  try {
3196
- if (file.originalPath) {
3197
- spawn3(cmd, ["--diff", file.originalPath, file.proposedPath], {
3198
- stdio: "ignore",
3199
- detached: true,
3200
- ...IS_WINDOWS3 && { shell: true }
3201
- }).unref();
3211
+ if (IS_WINDOWS3) {
3212
+ const quote = (s) => `"${s}"`;
3213
+ const parts = file.originalPath ? [cmd, "--diff", quote(file.originalPath), quote(file.proposedPath)] : [cmd, quote(file.proposedPath)];
3214
+ spawn3(parts.join(" "), { shell: true, stdio: "ignore", detached: true }).unref();
3202
3215
  } else {
3203
- spawn3(cmd, [file.proposedPath], {
3204
- stdio: "ignore",
3205
- detached: true,
3206
- ...IS_WINDOWS3 && { shell: true }
3207
- }).unref();
3216
+ const args = file.originalPath ? ["--diff", file.originalPath, file.proposedPath] : [file.proposedPath];
3217
+ spawn3(cmd, args, { stdio: "ignore", detached: true }).unref();
3208
3218
  }
3209
3219
  } catch {
3210
3220
  continue;
@@ -3593,7 +3603,8 @@ function getPrecommitBlock() {
3593
3603
  if [ -x "${bin}" ] || command -v "${bin}" >/dev/null 2>&1; then
3594
3604
  echo "\\033[2mcaliber: refreshing docs...\\033[0m"
3595
3605
  "${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
3606
+ "${bin}" learn finalize 2>/dev/null || true
3607
+ git diff --name-only -- CLAUDE.md .claude/ .cursor/ AGENTS.md CALIBER_LEARNINGS.md 2>/dev/null | xargs git add 2>/dev/null || true
3597
3608
  fi
3598
3609
  ${PRECOMMIT_END}`;
3599
3610
  }
@@ -3717,6 +3728,69 @@ function installLearningHooks() {
3717
3728
  writeSettings2(settings);
3718
3729
  return { installed: true, alreadyInstalled: false };
3719
3730
  }
3731
+ var CURSOR_HOOKS_PATH = path13.join(".cursor", "hooks.json");
3732
+ var CURSOR_HOOK_EVENTS = [
3733
+ { event: "postToolUse", tail: "learn observe" },
3734
+ { event: "postToolUseFailure", tail: "learn observe --failure" },
3735
+ { event: "sessionEnd", tail: "learn finalize" }
3736
+ ];
3737
+ function readCursorHooks() {
3738
+ if (!fs18.existsSync(CURSOR_HOOKS_PATH)) return { version: 1, hooks: {} };
3739
+ try {
3740
+ return JSON.parse(fs18.readFileSync(CURSOR_HOOKS_PATH, "utf-8"));
3741
+ } catch {
3742
+ return { version: 1, hooks: {} };
3743
+ }
3744
+ }
3745
+ function writeCursorHooks(config) {
3746
+ const dir = path13.dirname(CURSOR_HOOKS_PATH);
3747
+ if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
3748
+ fs18.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(config, null, 2));
3749
+ }
3750
+ function hasCursorHook(entries, tail) {
3751
+ return entries.some((e) => isCaliberCommand(e.command, tail));
3752
+ }
3753
+ function areCursorLearningHooksInstalled() {
3754
+ const config = readCursorHooks();
3755
+ return CURSOR_HOOK_EVENTS.every((cfg) => {
3756
+ const entries = config.hooks[cfg.event];
3757
+ return Array.isArray(entries) && hasCursorHook(entries, cfg.tail);
3758
+ });
3759
+ }
3760
+ function installCursorLearningHooks() {
3761
+ if (areCursorLearningHooksInstalled()) {
3762
+ return { installed: false, alreadyInstalled: true };
3763
+ }
3764
+ const config = readCursorHooks();
3765
+ const bin = resolveCaliber();
3766
+ for (const cfg of CURSOR_HOOK_EVENTS) {
3767
+ if (!Array.isArray(config.hooks[cfg.event])) {
3768
+ config.hooks[cfg.event] = [];
3769
+ }
3770
+ if (!hasCursorHook(config.hooks[cfg.event], cfg.tail)) {
3771
+ config.hooks[cfg.event].push({ command: `${bin} ${cfg.tail}` });
3772
+ }
3773
+ }
3774
+ writeCursorHooks(config);
3775
+ return { installed: true, alreadyInstalled: false };
3776
+ }
3777
+ function removeCursorLearningHooks() {
3778
+ const config = readCursorHooks();
3779
+ let removedAny = false;
3780
+ for (const cfg of CURSOR_HOOK_EVENTS) {
3781
+ const entries = config.hooks[cfg.event];
3782
+ if (!Array.isArray(entries)) continue;
3783
+ const idx = entries.findIndex((e) => isCaliberCommand(e.command, cfg.tail));
3784
+ if (idx !== -1) {
3785
+ entries.splice(idx, 1);
3786
+ removedAny = true;
3787
+ if (entries.length === 0) delete config.hooks[cfg.event];
3788
+ }
3789
+ }
3790
+ if (!removedAny) return { removed: false, notFound: true };
3791
+ writeCursorHooks(config);
3792
+ return { removed: true, notFound: false };
3793
+ }
3720
3794
  function removeLearningHooks() {
3721
3795
  const settings = readSettings2();
3722
3796
  if (!settings.hooks) return { removed: false, notFound: true };
@@ -3998,6 +4072,7 @@ var POINTS_PERMISSIONS = 2;
3998
4072
  var POINTS_HOOKS = 2;
3999
4073
  var POINTS_AGENTS_MD = 1;
4000
4074
  var POINTS_OPEN_SKILLS_FORMAT = 2;
4075
+ var POINTS_LEARNED_CONTENT = 2;
4001
4076
  var TOKEN_BUDGET_THRESHOLDS = [
4002
4077
  { maxTokens: 2e3, points: 6 },
4003
4078
  { maxTokens: 3500, points: 5 },
@@ -5106,6 +5181,18 @@ function checkBonus(dir) {
5106
5181
  instruction: "Migrate flat skill files to .claude/skills/{name}/SKILL.md with YAML frontmatter."
5107
5182
  } : void 0
5108
5183
  });
5184
+ const learningsContent = readFileOrNull2(join7(dir, "CALIBER_LEARNINGS.md"));
5185
+ const hasLearned = learningsContent ? learningsContent.split("\n").filter((l) => l.startsWith("- ")).length > 0 : false;
5186
+ checks.push({
5187
+ id: "learned_content",
5188
+ name: "Learned content present",
5189
+ category: "bonus",
5190
+ maxPoints: POINTS_LEARNED_CONTENT,
5191
+ earnedPoints: hasLearned ? POINTS_LEARNED_CONTENT : 0,
5192
+ passed: hasLearned,
5193
+ detail: hasLearned ? "Session learnings found in CALIBER_LEARNINGS.md" : "No learned content",
5194
+ suggestion: hasLearned ? void 0 : "Install learning hooks: `caliber learn install`"
5195
+ });
5109
5196
  return checks;
5110
5197
  }
5111
5198
 
@@ -7314,8 +7401,8 @@ async function scoreCommand(options) {
7314
7401
  }
7315
7402
 
7316
7403
  // src/commands/refresh.ts
7317
- import fs28 from "fs";
7318
- import path22 from "path";
7404
+ import fs29 from "fs";
7405
+ import path23 from "path";
7319
7406
  import chalk14 from "chalk";
7320
7407
  import ora5 from "ora";
7321
7408
 
@@ -7327,7 +7414,8 @@ var DOC_PATTERNS = [
7327
7414
  "README.md",
7328
7415
  ".cursorrules",
7329
7416
  ".cursor/rules/",
7330
- ".claude/skills/"
7417
+ ".claude/skills/",
7418
+ "CALIBER_LEARNINGS.md"
7331
7419
  ];
7332
7420
  function excludeArgs() {
7333
7421
  return DOC_PATTERNS.flatMap((p) => ["--", `:!${p}`]);
@@ -7431,8 +7519,8 @@ function writeRefreshDocs(docs) {
7431
7519
 
7432
7520
  // src/ai/refresh.ts
7433
7521
  init_config();
7434
- async function refreshDocs(diff, existingDocs, projectContext) {
7435
- const prompt = buildRefreshPrompt(diff, existingDocs, projectContext);
7522
+ async function refreshDocs(diff, existingDocs, projectContext, learnedSection) {
7523
+ const prompt = buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection);
7436
7524
  const fastModel = getFastModel();
7437
7525
  const raw = await llmCall({
7438
7526
  system: REFRESH_SYSTEM_PROMPT,
@@ -7442,7 +7530,7 @@ async function refreshDocs(diff, existingDocs, projectContext) {
7442
7530
  });
7443
7531
  return parseJsonResponse(raw);
7444
7532
  }
7445
- function buildRefreshPrompt(diff, existingDocs, projectContext) {
7533
+ function buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection) {
7446
7534
  const parts = [];
7447
7535
  parts.push("Update documentation based on the following code changes.\n");
7448
7536
  if (projectContext.packageName) parts.push(`Project: ${projectContext.packageName}`);
@@ -7490,9 +7578,144 @@ Changed files: ${diff.changedFiles.join(", ")}`);
7490
7578
  parts.push(rule.content);
7491
7579
  }
7492
7580
  }
7581
+ if (learnedSection) {
7582
+ parts.push("\n--- Learned Patterns (from session learning) ---");
7583
+ parts.push("Consider these accumulated learnings when deciding what to update:");
7584
+ parts.push(learnedSection);
7585
+ }
7493
7586
  return parts.join("\n");
7494
7587
  }
7495
7588
 
7589
+ // src/learner/writer.ts
7590
+ import fs27 from "fs";
7591
+ import path21 from "path";
7592
+ var LEARNINGS_FILE = "CALIBER_LEARNINGS.md";
7593
+ var LEARNINGS_HEADER = `# Caliber Learnings
7594
+
7595
+ Accumulated patterns and anti-patterns from development sessions.
7596
+ Auto-managed by [caliber](https://github.com/rely-ai-org/caliber) \u2014 do not edit manually.
7597
+
7598
+ `;
7599
+ var LEARNED_START = "<!-- caliber:learned -->";
7600
+ var LEARNED_END = "<!-- /caliber:learned -->";
7601
+ var MAX_LEARNED_ITEMS = 30;
7602
+ function writeLearnedContent(update) {
7603
+ const written = [];
7604
+ let newItemCount = 0;
7605
+ let newItems = [];
7606
+ if (update.claudeMdLearnedSection) {
7607
+ const result = writeLearnedSection(update.claudeMdLearnedSection);
7608
+ newItemCount = result.newCount;
7609
+ newItems = result.newItems;
7610
+ written.push(LEARNINGS_FILE);
7611
+ }
7612
+ if (update.skills?.length) {
7613
+ for (const skill of update.skills) {
7614
+ const skillPath = writeLearnedSkill(skill);
7615
+ written.push(skillPath);
7616
+ }
7617
+ }
7618
+ return { written, newItemCount, newItems };
7619
+ }
7620
+ function parseBullets(content) {
7621
+ const lines = content.split("\n");
7622
+ const bullets = [];
7623
+ let current = "";
7624
+ for (const line of lines) {
7625
+ if (line.startsWith("- ")) {
7626
+ if (current) bullets.push(current);
7627
+ current = line;
7628
+ } else if (current && line.trim() && !line.startsWith("#")) {
7629
+ current += "\n" + line;
7630
+ } else {
7631
+ if (current) bullets.push(current);
7632
+ current = "";
7633
+ }
7634
+ }
7635
+ if (current) bullets.push(current);
7636
+ return bullets;
7637
+ }
7638
+ function normalizeBullet(bullet) {
7639
+ return bullet.replace(/^- /, "").replace(/`[^`]*`/g, "").replace(/\s+/g, " ").toLowerCase().trim();
7640
+ }
7641
+ function deduplicateLearnedItems(existing, incoming) {
7642
+ const existingBullets = existing ? parseBullets(existing) : [];
7643
+ const incomingBullets = parseBullets(incoming);
7644
+ const merged = [...existingBullets];
7645
+ const newItems = [];
7646
+ for (const bullet of incomingBullets) {
7647
+ const norm = normalizeBullet(bullet);
7648
+ if (!norm) continue;
7649
+ const isDup = merged.some((e) => {
7650
+ const eNorm = normalizeBullet(e);
7651
+ const shorter = Math.min(norm.length, eNorm.length);
7652
+ const longer = Math.max(norm.length, eNorm.length);
7653
+ if (!(eNorm.includes(norm) || norm.includes(eNorm))) return false;
7654
+ return shorter / longer > 0.7;
7655
+ });
7656
+ if (!isDup) {
7657
+ merged.push(bullet);
7658
+ newItems.push(bullet);
7659
+ }
7660
+ }
7661
+ const capped = merged.length > MAX_LEARNED_ITEMS ? merged.slice(-MAX_LEARNED_ITEMS) : merged;
7662
+ return { merged: capped.join("\n"), newCount: newItems.length, newItems };
7663
+ }
7664
+ function writeLearnedSection(content) {
7665
+ const existingSection = readLearnedSection();
7666
+ const { merged, newCount, newItems } = deduplicateLearnedItems(existingSection, content);
7667
+ fs27.writeFileSync(LEARNINGS_FILE, LEARNINGS_HEADER + merged + "\n");
7668
+ return { newCount, newItems };
7669
+ }
7670
+ function writeLearnedSkill(skill) {
7671
+ const skillDir = path21.join(".claude", "skills", skill.name);
7672
+ if (!fs27.existsSync(skillDir)) fs27.mkdirSync(skillDir, { recursive: true });
7673
+ const skillPath = path21.join(skillDir, "SKILL.md");
7674
+ if (!skill.isNew && fs27.existsSync(skillPath)) {
7675
+ const existing = fs27.readFileSync(skillPath, "utf-8");
7676
+ fs27.writeFileSync(skillPath, existing.trimEnd() + "\n\n" + skill.content);
7677
+ } else {
7678
+ const frontmatter = [
7679
+ "---",
7680
+ `name: ${skill.name}`,
7681
+ `description: ${skill.description}`,
7682
+ "---",
7683
+ ""
7684
+ ].join("\n");
7685
+ fs27.writeFileSync(skillPath, frontmatter + skill.content);
7686
+ }
7687
+ return skillPath;
7688
+ }
7689
+ function readLearnedSection() {
7690
+ if (fs27.existsSync(LEARNINGS_FILE)) {
7691
+ const content2 = fs27.readFileSync(LEARNINGS_FILE, "utf-8");
7692
+ const bullets = content2.split("\n").filter((l) => l.startsWith("- ")).join("\n");
7693
+ return bullets || null;
7694
+ }
7695
+ const claudeMdPath = "CLAUDE.md";
7696
+ if (!fs27.existsSync(claudeMdPath)) return null;
7697
+ const content = fs27.readFileSync(claudeMdPath, "utf-8");
7698
+ const startIdx = content.indexOf(LEARNED_START);
7699
+ const endIdx = content.indexOf(LEARNED_END);
7700
+ if (startIdx === -1 || endIdx === -1) return null;
7701
+ return content.slice(startIdx + LEARNED_START.length, endIdx).trim() || null;
7702
+ }
7703
+ function migrateInlineLearnings() {
7704
+ if (fs27.existsSync(LEARNINGS_FILE)) return false;
7705
+ const claudeMdPath = "CLAUDE.md";
7706
+ if (!fs27.existsSync(claudeMdPath)) return false;
7707
+ const content = fs27.readFileSync(claudeMdPath, "utf-8");
7708
+ const startIdx = content.indexOf(LEARNED_START);
7709
+ const endIdx = content.indexOf(LEARNED_END);
7710
+ if (startIdx === -1 || endIdx === -1) return false;
7711
+ const section = content.slice(startIdx + LEARNED_START.length, endIdx).trim();
7712
+ if (!section) return false;
7713
+ fs27.writeFileSync(LEARNINGS_FILE, LEARNINGS_HEADER + section + "\n");
7714
+ const cleaned = content.slice(0, startIdx) + content.slice(endIdx + LEARNED_END.length);
7715
+ fs27.writeFileSync(claudeMdPath, cleaned.replace(/\n{3,}/g, "\n\n").trim() + "\n");
7716
+ return true;
7717
+ }
7718
+
7496
7719
  // src/commands/refresh.ts
7497
7720
  init_config();
7498
7721
  function log2(quiet, ...args) {
@@ -7501,11 +7724,11 @@ function log2(quiet, ...args) {
7501
7724
  function discoverGitRepos(parentDir) {
7502
7725
  const repos = [];
7503
7726
  try {
7504
- const entries = fs28.readdirSync(parentDir, { withFileTypes: true });
7727
+ const entries = fs29.readdirSync(parentDir, { withFileTypes: true });
7505
7728
  for (const entry of entries) {
7506
7729
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
7507
- const childPath = path22.join(parentDir, entry.name);
7508
- if (fs28.existsSync(path22.join(childPath, ".git"))) {
7730
+ const childPath = path23.join(parentDir, entry.name);
7731
+ if (fs29.existsSync(path23.join(childPath, ".git"))) {
7509
7732
  repos.push(childPath);
7510
7733
  }
7511
7734
  }
@@ -7529,6 +7752,7 @@ async function refreshSingleRepo(repoDir, options) {
7529
7752
  }
7530
7753
  const spinner = quiet ? null : ora5(`${prefix}Analyzing changes...`).start();
7531
7754
  const existingDocs = readExistingConfigs(repoDir);
7755
+ const learnedSection = readLearnedSection();
7532
7756
  const fingerprint = await collectFingerprint(repoDir);
7533
7757
  const projectContext = {
7534
7758
  languages: fingerprint.languages,
@@ -7544,7 +7768,8 @@ async function refreshSingleRepo(repoDir, options) {
7544
7768
  summary: diff.summary
7545
7769
  },
7546
7770
  existingDocs,
7547
- projectContext
7771
+ projectContext,
7772
+ learnedSection
7548
7773
  );
7549
7774
  if (!response.docsUpdated || response.docsUpdated.length === 0) {
7550
7775
  spinner?.succeed(`${prefix}No doc updates needed`);
@@ -7606,7 +7831,7 @@ async function refreshCommand(options) {
7606
7831
  `));
7607
7832
  const originalDir = process.cwd();
7608
7833
  for (const repo of repos) {
7609
- const repoName = path22.basename(repo);
7834
+ const repoName = path23.basename(repo);
7610
7835
  try {
7611
7836
  process.chdir(repo);
7612
7837
  await refreshSingleRepo(repo, { ...options, label: repoName });
@@ -7830,6 +8055,7 @@ async function configCommand() {
7830
8055
  }
7831
8056
 
7832
8057
  // src/commands/learn.ts
8058
+ import fs31 from "fs";
7833
8059
  import chalk17 from "chalk";
7834
8060
 
7835
8061
  // src/learner/stdin.ts
@@ -7861,8 +8087,8 @@ function readStdin() {
7861
8087
 
7862
8088
  // src/learner/storage.ts
7863
8089
  init_constants();
7864
- import fs29 from "fs";
7865
- import path23 from "path";
8090
+ import fs30 from "fs";
8091
+ import path24 from "path";
7866
8092
  var MAX_RESPONSE_LENGTH = 2e3;
7867
8093
  var DEFAULT_STATE = {
7868
8094
  sessionId: null,
@@ -7870,15 +8096,15 @@ var DEFAULT_STATE = {
7870
8096
  lastAnalysisTimestamp: null
7871
8097
  };
7872
8098
  function ensureLearningDir() {
7873
- if (!fs29.existsSync(LEARNING_DIR)) {
7874
- fs29.mkdirSync(LEARNING_DIR, { recursive: true });
8099
+ if (!fs30.existsSync(LEARNING_DIR)) {
8100
+ fs30.mkdirSync(LEARNING_DIR, { recursive: true });
7875
8101
  }
7876
8102
  }
7877
8103
  function sessionFilePath() {
7878
- return path23.join(LEARNING_DIR, LEARNING_SESSION_FILE);
8104
+ return path24.join(LEARNING_DIR, LEARNING_SESSION_FILE);
7879
8105
  }
7880
8106
  function stateFilePath() {
7881
- return path23.join(LEARNING_DIR, LEARNING_STATE_FILE);
8107
+ return path24.join(LEARNING_DIR, LEARNING_STATE_FILE);
7882
8108
  }
7883
8109
  function truncateResponse(response) {
7884
8110
  const str = JSON.stringify(response);
@@ -7889,113 +8115,84 @@ function appendEvent(event) {
7889
8115
  ensureLearningDir();
7890
8116
  const truncated = { ...event, tool_response: truncateResponse(event.tool_response) };
7891
8117
  const filePath = sessionFilePath();
7892
- fs29.appendFileSync(filePath, JSON.stringify(truncated) + "\n");
8118
+ fs30.appendFileSync(filePath, JSON.stringify(truncated) + "\n");
7893
8119
  const count = getEventCount();
7894
8120
  if (count > LEARNING_MAX_EVENTS) {
7895
- const lines = fs29.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
8121
+ const lines = fs30.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
7896
8122
  const kept = lines.slice(lines.length - LEARNING_MAX_EVENTS);
7897
- fs29.writeFileSync(filePath, kept.join("\n") + "\n");
8123
+ fs30.writeFileSync(filePath, kept.join("\n") + "\n");
7898
8124
  }
7899
8125
  }
7900
8126
  function readAllEvents() {
7901
8127
  const filePath = sessionFilePath();
7902
- if (!fs29.existsSync(filePath)) return [];
7903
- const lines = fs29.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
8128
+ if (!fs30.existsSync(filePath)) return [];
8129
+ const lines = fs30.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
7904
8130
  return lines.map((line) => JSON.parse(line));
7905
8131
  }
7906
8132
  function getEventCount() {
7907
8133
  const filePath = sessionFilePath();
7908
- if (!fs29.existsSync(filePath)) return 0;
7909
- const content = fs29.readFileSync(filePath, "utf-8");
8134
+ if (!fs30.existsSync(filePath)) return 0;
8135
+ const content = fs30.readFileSync(filePath, "utf-8");
7910
8136
  return content.split("\n").filter(Boolean).length;
7911
8137
  }
7912
8138
  function clearSession() {
7913
8139
  const filePath = sessionFilePath();
7914
- if (fs29.existsSync(filePath)) fs29.unlinkSync(filePath);
8140
+ if (fs30.existsSync(filePath)) fs30.unlinkSync(filePath);
7915
8141
  }
7916
8142
  function readState2() {
7917
8143
  const filePath = stateFilePath();
7918
- if (!fs29.existsSync(filePath)) return { ...DEFAULT_STATE };
8144
+ if (!fs30.existsSync(filePath)) return { ...DEFAULT_STATE };
7919
8145
  try {
7920
- return JSON.parse(fs29.readFileSync(filePath, "utf-8"));
8146
+ return JSON.parse(fs30.readFileSync(filePath, "utf-8"));
7921
8147
  } catch {
7922
8148
  return { ...DEFAULT_STATE };
7923
8149
  }
7924
8150
  }
7925
8151
  function writeState2(state) {
7926
8152
  ensureLearningDir();
7927
- fs29.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
8153
+ fs30.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
7928
8154
  }
7929
8155
  function resetState() {
7930
8156
  writeState2({ ...DEFAULT_STATE });
7931
8157
  }
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);
8158
+ var LOCK_FILE2 = "finalize.lock";
8159
+ var LOCK_STALE_MS = 5 * 60 * 1e3;
8160
+ function lockFilePath() {
8161
+ return path24.join(LEARNING_DIR, LOCK_FILE2);
8162
+ }
8163
+ function acquireFinalizeLock() {
8164
+ ensureLearningDir();
8165
+ const lockPath = lockFilePath();
8166
+ if (fs30.existsSync(lockPath)) {
8167
+ try {
8168
+ const stat = fs30.statSync(lockPath);
8169
+ if (Date.now() - stat.mtimeMs < LOCK_STALE_MS) {
8170
+ return false;
8171
+ }
8172
+ } catch {
7948
8173
  }
7949
8174
  }
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";
8175
+ try {
8176
+ fs30.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
8177
+ return true;
8178
+ } catch {
8179
+ try {
8180
+ const stat = fs30.statSync(lockPath);
8181
+ if (Date.now() - stat.mtimeMs >= LOCK_STALE_MS) {
8182
+ fs30.writeFileSync(lockPath, String(process.pid));
8183
+ return true;
8184
+ }
8185
+ } catch {
8186
+ }
8187
+ return false;
7969
8188
  }
7970
- fs30.writeFileSync(claudeMdPath, updated);
7971
8189
  }
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);
8190
+ function releaseFinalizeLock() {
8191
+ const lockPath = lockFilePath();
8192
+ try {
8193
+ if (fs30.existsSync(lockPath)) fs30.unlinkSync(lockPath);
8194
+ } catch {
7988
8195
  }
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
8196
  }
8000
8197
 
8001
8198
  // src/ai/learn.ts
@@ -8074,6 +8271,7 @@ ${eventsText}`;
8074
8271
 
8075
8272
  // src/commands/learn.ts
8076
8273
  init_config();
8274
+ var MIN_EVENTS_FOR_ANALYSIS = 50;
8077
8275
  async function learnObserveCommand(options) {
8078
8276
  try {
8079
8277
  const raw = await readStdin();
@@ -8081,11 +8279,11 @@ async function learnObserveCommand(options) {
8081
8279
  const hookData = JSON.parse(raw);
8082
8280
  const event = {
8083
8281
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8084
- session_id: hookData.session_id || "unknown",
8282
+ session_id: hookData.session_id || hookData.conversation_id || "unknown",
8085
8283
  hook_event_name: options.failure ? "PostToolUseFailure" : "PostToolUse",
8086
8284
  tool_name: hookData.tool_name || "unknown",
8087
8285
  tool_input: hookData.tool_input || {},
8088
- tool_response: hookData.tool_response || {},
8286
+ tool_response: hookData.tool_response || hookData.tool_output || {},
8089
8287
  tool_use_id: hookData.tool_use_id || "",
8090
8288
  cwd: hookData.cwd || process.cwd()
8091
8289
  };
@@ -8100,6 +8298,8 @@ async function learnObserveCommand(options) {
8100
8298
  async function learnFinalizeCommand() {
8101
8299
  const { isCaliberRunning: isCaliberRunning2 } = await Promise.resolve().then(() => (init_lock(), lock_exports));
8102
8300
  if (isCaliberRunning2()) return;
8301
+ if (!acquireFinalizeLock()) return;
8302
+ let analyzed = false;
8103
8303
  try {
8104
8304
  const config = loadConfig();
8105
8305
  if (!config) {
@@ -8108,12 +8308,9 @@ async function learnFinalizeCommand() {
8108
8308
  return;
8109
8309
  }
8110
8310
  const events = readAllEvents();
8111
- if (!events.length) {
8112
- clearSession();
8113
- resetState();
8114
- return;
8115
- }
8116
- await validateModel();
8311
+ if (events.length < MIN_EVENTS_FOR_ANALYSIS) return;
8312
+ await validateModel({ fast: true });
8313
+ migrateInlineLearnings();
8117
8314
  const existingConfigs = readExistingConfigs(process.cwd());
8118
8315
  const existingLearnedSection = readLearnedSection();
8119
8316
  const existingSkills = existingConfigs.claudeSkills || [];
@@ -8123,51 +8320,97 @@ async function learnFinalizeCommand() {
8123
8320
  existingLearnedSection,
8124
8321
  existingSkills
8125
8322
  );
8323
+ analyzed = true;
8126
8324
  if (response.claudeMdLearnedSection || response.skills?.length) {
8127
- writeLearnedContent({
8325
+ const result = writeLearnedContent({
8128
8326
  claudeMdLearnedSection: response.claudeMdLearnedSection,
8129
8327
  skills: response.skills
8130
8328
  });
8329
+ if (result.newItemCount > 0) {
8330
+ console.log(chalk17.dim(`caliber: learned ${result.newItemCount} new pattern${result.newItemCount === 1 ? "" : "s"}`));
8331
+ for (const item of result.newItems) {
8332
+ console.log(chalk17.dim(` + ${item.replace(/^- /, "").slice(0, 80)}`));
8333
+ }
8334
+ }
8131
8335
  }
8132
8336
  } catch {
8133
8337
  } finally {
8134
- clearSession();
8135
- resetState();
8338
+ if (analyzed) {
8339
+ clearSession();
8340
+ resetState();
8341
+ }
8342
+ releaseFinalizeLock();
8136
8343
  }
8137
8344
  }
8138
8345
  async function learnInstallCommand() {
8139
- const result = installLearningHooks();
8140
- if (result.alreadyInstalled) {
8141
- console.log(chalk17.dim("Learning hooks already installed."));
8346
+ let anyInstalled = false;
8347
+ if (fs31.existsSync(".claude")) {
8348
+ const r = installLearningHooks();
8349
+ if (r.installed) {
8350
+ console.log(chalk17.green("\u2713") + " Claude Code learning hooks installed");
8351
+ anyInstalled = true;
8352
+ } else if (r.alreadyInstalled) {
8353
+ console.log(chalk17.dim(" Claude Code hooks already installed"));
8354
+ }
8355
+ }
8356
+ if (fs31.existsSync(".cursor")) {
8357
+ const r = installCursorLearningHooks();
8358
+ if (r.installed) {
8359
+ console.log(chalk17.green("\u2713") + " Cursor learning hooks installed");
8360
+ anyInstalled = true;
8361
+ } else if (r.alreadyInstalled) {
8362
+ console.log(chalk17.dim(" Cursor hooks already installed"));
8363
+ }
8364
+ }
8365
+ if (!fs31.existsSync(".claude") && !fs31.existsSync(".cursor")) {
8366
+ console.log(chalk17.yellow("No .claude/ or .cursor/ directory found."));
8367
+ console.log(chalk17.dim(" Run `caliber init` first, or create the directory manually."));
8142
8368
  return;
8143
8369
  }
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."));
8370
+ if (anyInstalled) {
8371
+ console.log(chalk17.dim(` Tool usage will be recorded and learnings extracted after \u2265${MIN_EVENTS_FOR_ANALYSIS} events.`));
8372
+ console.log(chalk17.dim(" Learnings written to CALIBER_LEARNINGS.md."));
8373
+ }
8147
8374
  }
8148
8375
  async function learnRemoveCommand() {
8149
- const result = removeLearningHooks();
8150
- if (result.notFound) {
8151
- console.log(chalk17.dim("Learning hooks not found."));
8152
- return;
8376
+ let anyRemoved = false;
8377
+ const r1 = removeLearningHooks();
8378
+ if (r1.removed) {
8379
+ console.log(chalk17.green("\u2713") + " Claude Code learning hooks removed");
8380
+ anyRemoved = true;
8381
+ }
8382
+ const r2 = removeCursorLearningHooks();
8383
+ if (r2.removed) {
8384
+ console.log(chalk17.green("\u2713") + " Cursor learning hooks removed");
8385
+ anyRemoved = true;
8386
+ }
8387
+ if (!anyRemoved) {
8388
+ console.log(chalk17.dim("No learning hooks found."));
8153
8389
  }
8154
- console.log(chalk17.green("\u2713") + " Learning hooks removed from .claude/settings.json");
8155
8390
  }
8156
8391
  async function learnStatusCommand() {
8157
- const installed = areLearningHooksInstalled();
8392
+ const claudeInstalled = areLearningHooksInstalled();
8393
+ const cursorInstalled = areCursorLearningHooksInstalled();
8158
8394
  const state = readState2();
8159
8395
  const eventCount = getEventCount();
8160
8396
  console.log(chalk17.bold("Session Learning Status"));
8161
8397
  console.log();
8162
- if (installed) {
8163
- console.log(chalk17.green("\u2713") + " Learning hooks are " + chalk17.green("installed"));
8398
+ if (claudeInstalled) {
8399
+ console.log(chalk17.green("\u2713") + " Claude Code hooks " + chalk17.green("installed"));
8400
+ } else {
8401
+ console.log(chalk17.dim("\u2717") + " Claude Code hooks " + chalk17.dim("not installed"));
8402
+ }
8403
+ if (cursorInstalled) {
8404
+ console.log(chalk17.green("\u2713") + " Cursor hooks " + chalk17.green("installed"));
8164
8405
  } else {
8165
- console.log(chalk17.dim("\u2717") + " Learning hooks are " + chalk17.yellow("not installed"));
8406
+ console.log(chalk17.dim("\u2717") + " Cursor hooks " + chalk17.dim("not installed"));
8407
+ }
8408
+ if (!claudeInstalled && !cursorInstalled) {
8166
8409
  console.log(chalk17.dim(" Run `caliber learn install` to enable session learning."));
8167
8410
  }
8168
8411
  console.log();
8169
8412
  console.log(`Events recorded: ${chalk17.cyan(String(eventCount))}`);
8170
- console.log(`Total this session: ${chalk17.cyan(String(state.eventCount))}`);
8413
+ console.log(`Threshold for analysis: ${chalk17.cyan(String(MIN_EVENTS_FOR_ANALYSIS))}`);
8171
8414
  if (state.lastAnalysisTimestamp) {
8172
8415
  console.log(`Last analysis: ${chalk17.cyan(state.lastAnalysisTimestamp)}`);
8173
8416
  } else {
@@ -8177,14 +8420,14 @@ async function learnStatusCommand() {
8177
8420
  if (learnedSection) {
8178
8421
  const lineCount = learnedSection.split("\n").filter(Boolean).length;
8179
8422
  console.log(`
8180
- Learned items in CLAUDE.md: ${chalk17.cyan(String(lineCount))}`);
8423
+ Learned items in CALIBER_LEARNINGS.md: ${chalk17.cyan(String(lineCount))}`);
8181
8424
  }
8182
8425
  }
8183
8426
 
8184
8427
  // src/cli.ts
8185
8428
  var __dirname = path25.dirname(fileURLToPath(import.meta.url));
8186
8429
  var pkg = JSON.parse(
8187
- fs31.readFileSync(path25.resolve(__dirname, "..", "package.json"), "utf-8")
8430
+ fs32.readFileSync(path25.resolve(__dirname, "..", "package.json"), "utf-8")
8188
8431
  );
8189
8432
  var program = new Command();
8190
8433
  var displayVersion = process.env.CALIBER_LOCAL ? `${pkg.version}-local` : pkg.version;
@@ -8258,7 +8501,7 @@ learn.command("remove").description("Remove learning hooks from .claude/settings
8258
8501
  learn.command("status").description("Show learning system status").action(tracked("learn:status", learnStatusCommand));
8259
8502
 
8260
8503
  // src/utils/version-check.ts
8261
- import fs32 from "fs";
8504
+ import fs33 from "fs";
8262
8505
  import path26 from "path";
8263
8506
  import { fileURLToPath as fileURLToPath2 } from "url";
8264
8507
  import { execSync as execSync14 } from "child_process";
@@ -8267,13 +8510,13 @@ import ora6 from "ora";
8267
8510
  import confirm2 from "@inquirer/confirm";
8268
8511
  var __dirname_vc = path26.dirname(fileURLToPath2(import.meta.url));
8269
8512
  var pkg2 = JSON.parse(
8270
- fs32.readFileSync(path26.resolve(__dirname_vc, "..", "package.json"), "utf-8")
8513
+ fs33.readFileSync(path26.resolve(__dirname_vc, "..", "package.json"), "utf-8")
8271
8514
  );
8272
8515
  function getInstalledVersion() {
8273
8516
  try {
8274
8517
  const globalRoot = execSync14("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8275
8518
  const pkgPath = path26.join(globalRoot, "@rely-ai", "caliber", "package.json");
8276
- return JSON.parse(fs32.readFileSync(pkgPath, "utf-8")).version;
8519
+ return JSON.parse(fs33.readFileSync(pkgPath, "utf-8")).version;
8277
8520
  } catch {
8278
8521
  return null;
8279
8522
  }
@@ -8362,7 +8605,9 @@ acquireLock();
8362
8605
  if (process.env.CALIBER_LOCAL) {
8363
8606
  process.env.CALIBER_SKIP_UPDATE_CHECK = "1";
8364
8607
  }
8365
- var isQuickExit = ["--version", "-V", "--help", "-h"].some((f) => process.argv.includes(f));
8608
+ var userArgs = process.argv.slice(2);
8609
+ var hasCommand = userArgs.some((a) => !a.startsWith("-"));
8610
+ var isQuickExit = !hasCommand || ["--version", "-V", "--help", "-h"].some((f) => userArgs.includes(f));
8366
8611
  if (!isQuickExit) {
8367
8612
  await checkForUpdates();
8368
8613
  }
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.1",
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": {