@rely-ai/caliber 1.37.1 → 1.39.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 +718 -327
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -1099,14 +1099,14 @@ __export(lock_exports, {
1099
1099
  isCaliberRunning: () => isCaliberRunning,
1100
1100
  releaseLock: () => releaseLock
1101
1101
  });
1102
- import fs39 from "fs";
1103
- import path32 from "path";
1102
+ import fs40 from "fs";
1103
+ import path33 from "path";
1104
1104
  import os8 from "os";
1105
1105
  import crypto5 from "crypto";
1106
1106
  function buildLockPath() {
1107
1107
  const cwd = process.cwd();
1108
1108
  const hash = crypto5.createHash("md5").update(cwd).digest("hex").slice(0, 8);
1109
- return path32.join(os8.tmpdir(), `.caliber-${hash}.lock`);
1109
+ return path33.join(os8.tmpdir(), `.caliber-${hash}.lock`);
1110
1110
  }
1111
1111
  function getLockFile() {
1112
1112
  if (!_lockPath) _lockPath = buildLockPath();
@@ -1115,8 +1115,8 @@ function getLockFile() {
1115
1115
  function isCaliberRunning() {
1116
1116
  try {
1117
1117
  const lockFile = buildLockPath();
1118
- if (!fs39.existsSync(lockFile)) return false;
1119
- const raw = fs39.readFileSync(lockFile, "utf-8").trim();
1118
+ if (!fs40.existsSync(lockFile)) return false;
1119
+ const raw = fs40.readFileSync(lockFile, "utf-8").trim();
1120
1120
  const { pid, ts } = JSON.parse(raw);
1121
1121
  if (Date.now() - ts > STALE_MS) return false;
1122
1122
  try {
@@ -1131,14 +1131,14 @@ function isCaliberRunning() {
1131
1131
  }
1132
1132
  function acquireLock() {
1133
1133
  try {
1134
- fs39.writeFileSync(getLockFile(), JSON.stringify({ pid: process.pid, ts: Date.now() }));
1134
+ fs40.writeFileSync(getLockFile(), JSON.stringify({ pid: process.pid, ts: Date.now() }));
1135
1135
  } catch {
1136
1136
  }
1137
1137
  }
1138
1138
  function releaseLock() {
1139
1139
  try {
1140
1140
  const lockFile = getLockFile();
1141
- if (fs39.existsSync(lockFile)) fs39.unlinkSync(lockFile);
1141
+ if (fs40.existsSync(lockFile)) fs40.unlinkSync(lockFile);
1142
1142
  } catch {
1143
1143
  }
1144
1144
  }
@@ -1153,8 +1153,8 @@ var init_lock = __esm({
1153
1153
 
1154
1154
  // src/cli.ts
1155
1155
  import { Command } from "commander";
1156
- import fs50 from "fs";
1157
- import path41 from "path";
1156
+ import fs51 from "fs";
1157
+ import path42 from "path";
1158
1158
  import { fileURLToPath } from "url";
1159
1159
 
1160
1160
  // src/commands/init.ts
@@ -1309,6 +1309,19 @@ var REFRESH_LAST_ERROR_FILE = path2.join(CALIBER_DIR, "last-refresh-error.json")
1309
1309
  var MIN_SESSIONS_FOR_COMPARISON = 3;
1310
1310
 
1311
1311
  // src/fingerprint/existing-config.ts
1312
+ var CALIBER_MANAGED_PREFIX = "caliber-";
1313
+ var INCLUDABLE_DOC_PATTERNS = [
1314
+ "ARCHITECTURE.md",
1315
+ "CONTRIBUTING.md",
1316
+ "DEVELOPMENT.md",
1317
+ "SETUP.md",
1318
+ "docs/ARCHITECTURE.md",
1319
+ "docs/CONTRIBUTING.md",
1320
+ "docs/DEVELOPMENT.md",
1321
+ "docs/API.md",
1322
+ "docs/GUIDE.md",
1323
+ "docs/SETUP.md"
1324
+ ];
1312
1325
  function readSkillsFromDir(skillsDir) {
1313
1326
  if (!fs2.existsSync(skillsDir)) return void 0;
1314
1327
  try {
@@ -1371,6 +1384,18 @@ function readExistingConfigs(dir) {
1371
1384
  } catch {
1372
1385
  }
1373
1386
  }
1387
+ const claudeRulesDir = path3.join(dir, ".claude", "rules");
1388
+ if (fs2.existsSync(claudeRulesDir)) {
1389
+ try {
1390
+ const files = fs2.readdirSync(claudeRulesDir).filter((f) => f.endsWith(".md"));
1391
+ const rules = files.map((f) => ({
1392
+ filename: f,
1393
+ content: fs2.readFileSync(path3.join(claudeRulesDir, f), "utf-8")
1394
+ }));
1395
+ if (rules.length > 0) configs.claudeRules = rules;
1396
+ } catch {
1397
+ }
1398
+ }
1374
1399
  const cursorrulesPath = path3.join(dir, ".cursorrules");
1375
1400
  if (fs2.existsSync(cursorrulesPath)) {
1376
1401
  configs.cursorrules = fs2.readFileSync(cursorrulesPath, "utf-8");
@@ -1434,6 +1459,8 @@ function readExistingConfigs(dir) {
1434
1459
  } catch {
1435
1460
  }
1436
1461
  }
1462
+ const found = INCLUDABLE_DOC_PATTERNS.filter((p) => fs2.existsSync(path3.join(dir, p)));
1463
+ if (found.length > 0) configs.includableDocs = found;
1437
1464
  return configs;
1438
1465
  }
1439
1466
 
@@ -2708,15 +2735,16 @@ var CLAUDE_CLI_BIN = "claude";
2708
2735
  var DEFAULT_TIMEOUT_MS2 = 10 * 60 * 1e3;
2709
2736
  var IS_WINDOWS2 = process.platform === "win32";
2710
2737
  function spawnClaude(args) {
2738
+ const env = { ...process.env, CLAUDE_CODE_SIMPLE: "1" };
2711
2739
  return IS_WINDOWS2 ? spawn2([CLAUDE_CLI_BIN, ...args].join(" "), {
2712
2740
  cwd: process.cwd(),
2713
2741
  stdio: ["pipe", "pipe", "pipe"],
2714
- env: process.env,
2742
+ env,
2715
2743
  shell: true
2716
2744
  }) : spawn2(CLAUDE_CLI_BIN, args, {
2717
2745
  cwd: process.cwd(),
2718
2746
  stdio: ["pipe", "pipe", "pipe"],
2719
- env: process.env
2747
+ env
2720
2748
  });
2721
2749
  }
2722
2750
  var ClaudeCliProvider = class {
@@ -3113,6 +3141,7 @@ var CONFIG_FILE_TYPES = `You understand these config files:
3113
3141
  - .agents/skills/{name}/SKILL.md: Same OpenSkills format for Codex skills (Codex scans .agents/skills/ for skills).
3114
3142
  - .opencode/skills/{name}/SKILL.md: Same OpenSkills format for OpenCode skills (OpenCode scans .opencode/skills/ for skills).
3115
3143
  - .cursor/skills/{name}/SKILL.md: Same OpenSkills format for Cursor skills.
3144
+ - .claude/rules/*.md: Path-scoped rules for Claude Code with YAML frontmatter. Each rule file contains a \`paths:\` field with glob patterns \u2014 Claude Code only loads the rule when the user works on matching files. Use for domain-specific conventions (e.g., API patterns, test conventions, database rules). Always-apply rules omit the \`paths:\` field.
3116
3145
  - .cursorrules: Coding rules for Cursor (deprecated legacy format \u2014 do NOT generate this).
3117
3146
  - .cursor/rules/*.mdc: Modern Cursor rules with frontmatter (description, globs, alwaysApply).
3118
3147
  - .github/copilot-instructions.md: Always-on repository-wide instructions for GitHub Copilot \u2014 same purpose as CLAUDE.md but for Copilot. Plain markdown, no frontmatter.
@@ -3152,6 +3181,7 @@ Skill field requirements:
3152
3181
  - "name" MUST NOT be any of these reserved names (they are managed by Caliber automatically): "setup-caliber", "find-skills", "save-learning". Do NOT generate skills with these names.
3153
3182
  - "description": MUST include WHAT it does + WHEN to use it with specific trigger phrases. Example: "Manages database migrations. Use when user says 'run migration', 'create migration', 'db schema change', or modifies files in db/migrations/."
3154
3183
  - "content": markdown body only \u2014 do NOT include YAML frontmatter, it is generated from name+description.
3184
+ - "paths" (optional): array of glob patterns. When provided, the skill is only loaded when the user works on matching files. Use for skills tied to specific file types or directories. Example: ["src/api/**", "src/routes/**"] for an API skill, or ["Dockerfile*", "docker-compose*"] for a Docker skill. Omit for general-purpose skills.
3155
3185
 
3156
3186
  Skill content structure \u2014 follow this template:
3157
3187
  1. A heading with the skill name
@@ -3160,6 +3190,31 @@ Skill content structure \u2014 follow this template:
3160
3190
  4. "## Troubleshooting" (optional) \u2014 common errors and how to fix them
3161
3191
 
3162
3192
  Keep skill content under 200 lines. Focus on actionable instructions, not documentation prose.`;
3193
+ var CLAUDE_RULES_FORMAT = `CLAUDE RULES FORMAT \u2014 .claude/rules/*.md files:
3194
+ Claude Code loads .claude/rules/*.md files as path-scoped instructions. Generate 2-4 rules that extract domain-specific conventions from CLAUDE.md into focused, contextually-loaded files.
3195
+
3196
+ Each rule file uses YAML frontmatter with an optional \`paths:\` field:
3197
+ \`\`\`markdown
3198
+ ---
3199
+ paths:
3200
+ - src/api/**
3201
+ - src/routes/**
3202
+ ---
3203
+
3204
+ # API Conventions
3205
+
3206
+ - All endpoints return \`{ data, error }\` envelope
3207
+ - Use \`validateRequest(schema)\` middleware before handlers
3208
+ \`\`\`
3209
+
3210
+ Rules without \`paths:\` apply globally (use sparingly \u2014 prefer path-scoped rules).
3211
+
3212
+ Guidelines:
3213
+ - Extract patterns from the codebase that apply to specific file types or directories
3214
+ - Keep each rule under 50 lines \u2014 these are loaded into context, conciseness matters
3215
+ - Good candidates: testing patterns, API conventions, database query patterns, component structure
3216
+ - Do NOT duplicate content from CLAUDE.md \u2014 rules supplement it with path-specific detail
3217
+ - filename must be kebab-case ending in .md (e.g. \`testing-patterns.md\`, \`api-conventions.md\`)`;
3163
3218
  var SCORING_CRITERIA = `SCORING CRITERIA \u2014 your output is scored deterministically against the actual filesystem. Optimize for 100/100:
3164
3219
 
3165
3220
  Existence (25 pts):
@@ -3202,7 +3257,13 @@ For command sections, use code blocks with one command per line.
3202
3257
 
3203
3258
  - Each skill content: max 150 lines. Focus on patterns and examples, not exhaustive docs.
3204
3259
  - Cursor rules: max 5 .mdc files.
3205
- - If the project is large, prioritize depth on the 3-4 most critical tools over breadth across everything.`;
3260
+ - If the project is large, prioritize depth on the 3-4 most critical tools over breadth across everything.
3261
+
3262
+ @include directives (Claude Code only):
3263
+ - If the project has existing documentation files (ARCHITECTURE.md, CONTRIBUTING.md, API docs, etc.), reference them in CLAUDE.md using \`@./path\` instead of summarizing their content. Claude Code will inline them automatically.
3264
+ - Example: Instead of writing an architecture summary, add \`@./ARCHITECTURE.md\` in the relevant section.
3265
+ - This keeps CLAUDE.md compact while giving the agent access to detailed docs.
3266
+ - Only use @include for files that actually exist in the provided file tree.`;
3206
3267
  var GENERATION_SYSTEM_PROMPT = `${ROLE_AND_CONTEXT}
3207
3268
 
3208
3269
  ${CONFIG_FILE_TYPES}
@@ -3224,18 +3285,19 @@ AgentSetup schema:
3224
3285
  ],
3225
3286
  "claude": {
3226
3287
  "claudeMd": "string (markdown content for CLAUDE.md)",
3227
- "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)" }]
3288
+ "rules": [{ "filename": "string.md (kebab-case, e.g. api-conventions.md)", "content": "string (markdown with YAML frontmatter containing paths: glob)" }],
3289
+ "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)", "paths": ["optional array of glob patterns \u2014 omit for general-purpose skills"] }]
3228
3290
  },
3229
3291
  "codex": {
3230
3292
  "agentsMd": "string (markdown content for AGENTS.md \u2014 the primary Codex instructions file, same quality/structure as CLAUDE.md)",
3231
- "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)" }]
3293
+ "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)", "paths": ["optional array of glob patterns \u2014 omit for general-purpose skills"] }]
3232
3294
  },
3233
3295
  "opencode": {
3234
3296
  "agentsMd": "string (markdown content for AGENTS.md \u2014 reuse codex.agentsMd if codex is also targeted, otherwise generate fresh)",
3235
- "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)" }]
3297
+ "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)", "paths": ["optional array of glob patterns \u2014 omit for general-purpose skills"] }]
3236
3298
  },
3237
3299
  "cursor": {
3238
- "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)" }],
3300
+ "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)", "paths": ["optional array of glob patterns \u2014 omit for general-purpose skills"] }],
3239
3301
  "rules": [{ "filename": "string.mdc", "content": "string (with frontmatter)" }]
3240
3302
  },
3241
3303
  "copilot": {
@@ -3248,12 +3310,15 @@ NOTE: If both "codex" and "opencode" are targeted, set opencode.agentsMd to the
3248
3310
 
3249
3311
  ${SKILL_FORMAT_RULES}
3250
3312
 
3313
+ ${CLAUDE_RULES_FORMAT}
3314
+
3251
3315
  ${FILE_DESCRIPTIONS_RULES}
3252
3316
 
3253
3317
  ${SCORING_CRITERIA}
3254
3318
 
3255
3319
  ${OUTPUT_SIZE_CONSTRAINTS}
3256
- - Skills: generate 3-6 skills per target platform based on project complexity. Each skill should cover a distinct tool, workflow, or domain \u2014 don't pad with generic skills.`;
3320
+ - Skills: generate 3-6 skills per target platform based on project complexity. Each skill should cover a distinct tool, workflow, or domain \u2014 don't pad with generic skills.
3321
+ - Claude rules: generate 2-4 .claude/rules/*.md files when claude is targeted. Each rule should extract domain-specific patterns from the codebase.`;
3257
3322
  var CORE_GENERATION_PROMPT = `${ROLE_AND_CONTEXT}
3258
3323
 
3259
3324
  ${CONFIG_FILE_TYPES}
@@ -3275,6 +3340,7 @@ CoreSetup schema:
3275
3340
  ],
3276
3341
  "claude": {
3277
3342
  "claudeMd": "string (markdown content for CLAUDE.md)",
3343
+ "rules": [{ "filename": "string.md (kebab-case, e.g. api-conventions.md)", "content": "string (markdown with YAML frontmatter containing paths: glob)" }],
3278
3344
  "skillTopics": [{ "name": "string (kebab-case)", "description": "string (what this skill does and WHEN to use it \u2014 include trigger phrases)" }]
3279
3345
  },
3280
3346
  "codex": {
@@ -3308,12 +3374,15 @@ Skill topic description MUST follow this formula: [What it does] + [When to use
3308
3374
  Include specific trigger phrases users would actually say. Also include negative triggers to prevent over-triggering.
3309
3375
  Example: "Creates a new API endpoint following the project's route pattern. Handles request validation, error responses, and DB queries. Use when user says 'add endpoint', 'new route', 'create API', or adds files to src/routes/. Do NOT use for modifying existing routes."
3310
3376
 
3377
+ ${CLAUDE_RULES_FORMAT}
3378
+
3311
3379
  ${FILE_DESCRIPTIONS_RULES}
3312
3380
 
3313
3381
  ${SCORING_CRITERIA}
3314
3382
 
3315
3383
  ${OUTPUT_SIZE_CONSTRAINTS}
3316
- - Skill topics: 3-6 per platform based on project complexity (name + description only, no content).`;
3384
+ - Skill topics: 3-6 per platform based on project complexity (name + description only, no content).
3385
+ - Claude rules: generate 2-4 .claude/rules/*.md files when claude is targeted.`;
3317
3386
  var SKILL_GENERATION_PROMPT = `You generate a single skill file for a coding agent (Claude Code, Cursor, Codex, or OpenCode).
3318
3387
 
3319
3388
  Given project context and a skill topic, produce a focused SKILL.md body.
@@ -3343,7 +3412,7 @@ Rules:
3343
3412
  Description field formula: [What it does] + [When to use it with trigger phrases] + [Key capabilities]. Include negative triggers ("Do NOT use for X") to prevent over-triggering.
3344
3413
 
3345
3414
  Return ONLY a JSON object:
3346
- {"name": "string (kebab-case)", "description": "string (what + when + capabilities + negative triggers)", "content": "string (markdown body)"}`;
3415
+ {"name": "string (kebab-case)", "description": "string (what + when + capabilities + negative triggers)", "content": "string (markdown body)", "paths": ["optional glob patterns \u2014 include when the skill applies to specific file types or directories, omit for general-purpose skills"]}`;
3347
3416
  var REFINE_SYSTEM_PROMPT = `You are an expert at modifying coding agent configurations (Claude Code, Cursor, Codex, OpenCode, and GitHub Copilot).
3348
3417
 
3349
3418
  You will receive the current AgentSetup JSON and a user request describing what to change.
@@ -3400,7 +3469,7 @@ var REFRESH_SYSTEM_PROMPT = `You are an expert at maintaining coding project doc
3400
3469
 
3401
3470
  You will receive:
3402
3471
  1. Git diffs showing what code changed
3403
- 2. Current contents of documentation files (CLAUDE.md, README.md, skills, cursor rules, copilot instructions)
3472
+ 2. Current contents of documentation files (CLAUDE.md, .claude/rules/*.md, README.md, skills, cursor rules, copilot instructions)
3404
3473
  3. Project context (languages, frameworks, file tree)
3405
3474
 
3406
3475
  CONSERVATIVE UPDATE means:
@@ -3419,6 +3488,12 @@ Quality constraints (the output is scored deterministically):
3419
3488
  - ONLY reference file paths that exist in the provided file tree \u2014 do NOT invent paths
3420
3489
  - Preserve the existing structure (headings, bullet style, formatting)
3421
3490
 
3491
+ Claude rules (.claude/rules/*.md):
3492
+ - If the diff affects code in a domain covered by an existing rule, update that rule
3493
+ - If the diff introduces a new domain pattern (e.g., new API conventions, new test patterns), create a new rule file
3494
+ - Rules with paths: frontmatter should only contain conventions relevant to those paths
3495
+ - Keep rules under 50 lines \u2014 they load into context alongside other instructions
3496
+
3422
3497
  Cross-agent sync:
3423
3498
  - When a code change affects documentation, update ALL provided platform configs together.
3424
3499
  - A renamed command, moved file, or changed convention must be reflected in every config (CLAUDE.md, AGENTS.md, copilot instructions, skills across all platforms).
@@ -3435,6 +3510,7 @@ Return a JSON object with this exact shape:
3435
3510
  "updatedDocs": {
3436
3511
  "agentsMd": "<updated content or null>",
3437
3512
  "claudeMd": "<updated content or null>",
3513
+ "claudeRules": [{"filename": "name.md", "content": "full content with frontmatter"}] or null,
3438
3514
  "readmeMd": "<updated content or null>",
3439
3515
  "cursorrules": "<updated content or null>",
3440
3516
  "cursorRules": [{"filename": "name.mdc", "content": "..."}] or null,
@@ -3695,7 +3771,7 @@ function getDetectedWorkspaces(dir) {
3695
3771
 
3696
3772
  // src/fingerprint/index.ts
3697
3773
  async function collectFingerprint(dir) {
3698
- const gitRemoteUrl = getGitRemoteUrl();
3774
+ const gitRemoteUrl = getGitRemoteUrl(dir);
3699
3775
  const fileTree = getFileTree(dir);
3700
3776
  const existingConfigs = readExistingConfigs(dir);
3701
3777
  const packageName = readPackageName(dir);
@@ -4028,6 +4104,55 @@ function removeHook() {
4028
4104
  writeSettings(settings);
4029
4105
  return { removed: true, notFound: false };
4030
4106
  }
4107
+ function createScriptHook(config) {
4108
+ const { eventName, scriptPath, scriptContent, description } = config;
4109
+ const hasHook = (matchers) => matchers.some((entry) => entry.hooks?.some((h) => h.description === description));
4110
+ function isInstalled() {
4111
+ const settings = readSettings();
4112
+ const matchers = settings.hooks?.[eventName];
4113
+ return Array.isArray(matchers) && hasHook(matchers);
4114
+ }
4115
+ function install() {
4116
+ const settings = readSettings();
4117
+ if (!settings.hooks) settings.hooks = {};
4118
+ const matchers = settings.hooks[eventName];
4119
+ if (Array.isArray(matchers) && hasHook(matchers)) {
4120
+ return { installed: false, alreadyInstalled: true };
4121
+ }
4122
+ const scriptDir = path10.dirname(scriptPath);
4123
+ if (!fs10.existsSync(scriptDir)) fs10.mkdirSync(scriptDir, { recursive: true });
4124
+ fs10.writeFileSync(scriptPath, scriptContent);
4125
+ fs10.chmodSync(scriptPath, 493);
4126
+ if (!Array.isArray(settings.hooks[eventName])) {
4127
+ settings.hooks[eventName] = [];
4128
+ }
4129
+ settings.hooks[eventName].push({
4130
+ matcher: "",
4131
+ hooks: [{ type: "command", command: scriptPath, description }]
4132
+ });
4133
+ writeSettings(settings);
4134
+ return { installed: true, alreadyInstalled: false };
4135
+ }
4136
+ function remove() {
4137
+ const settings = readSettings();
4138
+ const matchers = settings.hooks?.[eventName];
4139
+ if (!Array.isArray(matchers)) return { removed: false, notFound: true };
4140
+ const idx = matchers.findIndex(
4141
+ (entry) => entry.hooks?.some((h) => h.description === description)
4142
+ );
4143
+ if (idx === -1) return { removed: false, notFound: true };
4144
+ matchers.splice(idx, 1);
4145
+ if (matchers.length === 0) delete settings.hooks[eventName];
4146
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
4147
+ writeSettings(settings);
4148
+ try {
4149
+ fs10.unlinkSync(scriptPath);
4150
+ } catch {
4151
+ }
4152
+ return { removed: true, notFound: false };
4153
+ }
4154
+ return { isInstalled, install, remove };
4155
+ }
4031
4156
  var STOP_HOOK_SCRIPT_CONTENT = `#!/bin/sh
4032
4157
  if grep -q "caliber" .git/hooks/pre-commit 2>/dev/null; then
4033
4158
  exit 0
@@ -4040,58 +4165,44 @@ fi
4040
4165
  touch "$FLAG"
4041
4166
  printf '{"decision":"block","reason":"Caliber agent config sync is not set up on this machine. Tell the user: This project uses Caliber to keep agent configs in sync \u2014 it is not set up yet. Want me to run /setup-caliber? (~30 seconds)"}'
4042
4167
  `;
4043
- var STOP_HOOK_SCRIPT_PATH = path10.join(".claude", "hooks", "caliber-check-sync.sh");
4044
- var STOP_HOOK_DESCRIPTION = "Caliber: offer setup if not configured";
4045
- function hasStopHook(matchers) {
4046
- return matchers.some(
4047
- (entry) => entry.hooks?.some((h) => h.description === STOP_HOOK_DESCRIPTION)
4048
- );
4049
- }
4050
- function installStopHook() {
4051
- const settings = readSettings();
4052
- if (!settings.hooks) settings.hooks = {};
4053
- const stop = settings.hooks.Stop;
4054
- if (Array.isArray(stop) && hasStopHook(stop)) {
4055
- return { installed: false, alreadyInstalled: true };
4056
- }
4057
- const scriptDir = path10.dirname(STOP_HOOK_SCRIPT_PATH);
4058
- if (!fs10.existsSync(scriptDir)) fs10.mkdirSync(scriptDir, { recursive: true });
4059
- fs10.writeFileSync(STOP_HOOK_SCRIPT_PATH, STOP_HOOK_SCRIPT_CONTENT);
4060
- fs10.chmodSync(STOP_HOOK_SCRIPT_PATH, 493);
4061
- if (!Array.isArray(settings.hooks.Stop)) {
4062
- settings.hooks.Stop = [];
4063
- }
4064
- settings.hooks.Stop.push({
4065
- matcher: "",
4066
- hooks: [
4067
- {
4068
- type: "command",
4069
- command: STOP_HOOK_SCRIPT_PATH,
4070
- description: STOP_HOOK_DESCRIPTION
4071
- }
4072
- ]
4073
- });
4074
- writeSettings(settings);
4075
- return { installed: true, alreadyInstalled: false };
4076
- }
4077
- function removeStopHook() {
4078
- const settings = readSettings();
4079
- const stop = settings.hooks?.Stop;
4080
- if (!Array.isArray(stop)) return { removed: false, notFound: true };
4081
- const idx = stop.findIndex(
4082
- (entry) => entry.hooks?.some((h) => h.description === STOP_HOOK_DESCRIPTION)
4083
- );
4084
- if (idx === -1) return { removed: false, notFound: true };
4085
- stop.splice(idx, 1);
4086
- if (stop.length === 0) delete settings.hooks.Stop;
4087
- if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
4088
- writeSettings(settings);
4089
- try {
4090
- fs10.unlinkSync(STOP_HOOK_SCRIPT_PATH);
4091
- } catch {
4092
- }
4093
- return { removed: true, notFound: false };
4094
- }
4168
+ var stopHook = createScriptHook({
4169
+ eventName: "Stop",
4170
+ scriptPath: path10.join(".claude", "hooks", "caliber-check-sync.sh"),
4171
+ scriptContent: STOP_HOOK_SCRIPT_CONTENT,
4172
+ description: "Caliber: offer setup if not configured"
4173
+ });
4174
+ var installStopHook = stopHook.install;
4175
+ var removeStopHook = stopHook.remove;
4176
+ var FRESHNESS_SCRIPT = `#!/bin/sh
4177
+ STATE_FILE=".caliber/.caliber-state.json"
4178
+ [ ! -f "$STATE_FILE" ] && exit 0
4179
+ LAST_SHA=$(grep -o '"lastRefreshSha":"[^"]*"' "$STATE_FILE" 2>/dev/null | cut -d'"' -f4)
4180
+ [ -z "$LAST_SHA" ] && exit 0
4181
+ CURRENT_SHA=$(git rev-parse HEAD 2>/dev/null)
4182
+ [ "$LAST_SHA" = "$CURRENT_SHA" ] && exit 0
4183
+ COMMITS_BEHIND=$(git rev-list --count "$LAST_SHA".."$CURRENT_SHA" 2>/dev/null || echo 0)
4184
+ if [ "$COMMITS_BEHIND" -gt 15 ]; then
4185
+ printf '{"systemMessage":"Caliber: agent configs are %s commits behind. Run caliber refresh to sync."}' "$COMMITS_BEHIND"
4186
+ fi
4187
+ `;
4188
+ var sessionStartHook = createScriptHook({
4189
+ eventName: "SessionStart",
4190
+ scriptPath: path10.join(".claude", "hooks", "caliber-session-freshness.sh"),
4191
+ scriptContent: FRESHNESS_SCRIPT,
4192
+ description: "Caliber: check config freshness on session start"
4193
+ });
4194
+ var isSessionStartHookInstalled = sessionStartHook.isInstalled;
4195
+ var installSessionStartHook = sessionStartHook.install;
4196
+ var removeSessionStartHook = sessionStartHook.remove;
4197
+ var notificationHook = createScriptHook({
4198
+ eventName: "Notification",
4199
+ scriptPath: path10.join(".claude", "hooks", "caliber-freshness-notify.sh"),
4200
+ scriptContent: FRESHNESS_SCRIPT,
4201
+ description: "Caliber: warn when agent configs are stale"
4202
+ });
4203
+ var isNotificationHookInstalled = notificationHook.isInstalled;
4204
+ var installNotificationHook = notificationHook.install;
4205
+ var removeNotificationHook = notificationHook.remove;
4095
4206
  var PRECOMMIT_START = "# caliber:pre-commit:start";
4096
4207
  var PRECOMMIT_END = "# caliber:pre-commit:end";
4097
4208
  function getPrecommitBlock() {
@@ -4698,15 +4809,15 @@ init_config();
4698
4809
  // src/utils/dependencies.ts
4699
4810
  import { readFileSync as readFileSync2 } from "fs";
4700
4811
  import { join as join2 } from "path";
4701
- function readFileOrNull2(path43) {
4812
+ function readFileOrNull2(path44) {
4702
4813
  try {
4703
- return readFileSync2(path43, "utf-8");
4814
+ return readFileSync2(path44, "utf-8");
4704
4815
  } catch {
4705
4816
  return null;
4706
4817
  }
4707
4818
  }
4708
- function readJsonOrNull(path43) {
4709
- const content = readFileOrNull2(path43);
4819
+ function readJsonOrNull(path44) {
4820
+ const content = readFileOrNull2(path44);
4710
4821
  if (!content) return null;
4711
4822
  try {
4712
4823
  return JSON.parse(content);
@@ -4972,7 +5083,8 @@ Generate the skill content following the instructions in the system prompt.`;
4972
5083
  return {
4973
5084
  name: result.name || topic.name,
4974
5085
  description: result.description || topic.description,
4975
- content
5086
+ content,
5087
+ ...result.paths?.length ? { paths: result.paths } : {}
4976
5088
  };
4977
5089
  }
4978
5090
  async function streamGeneration(config) {
@@ -5188,7 +5300,7 @@ function sampleFileTree(fileTree, codeAnalysisPaths, limit) {
5188
5300
  function buildGeneratePrompt(fingerprint, targetAgent, prompt, failingChecks, currentScore, passingChecks) {
5189
5301
  const parts = [];
5190
5302
  const existing = fingerprint.existingConfigs;
5191
- const hasExistingConfigs = !!(existing.claudeMd || existing.claudeSettings || existing.claudeSkills?.length || existing.readmeMd || existing.agentsMd || existing.cursorrules || existing.cursorRules?.length);
5303
+ const hasExistingConfigs = !!(existing.claudeMd || existing.claudeSettings || existing.claudeSkills?.length || existing.claudeRules?.length || existing.readmeMd || existing.agentsMd || existing.cursorrules || existing.cursorRules?.length);
5192
5304
  const isTargetedFix = failingChecks && failingChecks.length > 0 && currentScore !== void 0 && currentScore >= 95;
5193
5305
  if (isTargetedFix) {
5194
5306
  parts.push(`TARGETED FIX MODE \u2014 current score: ${currentScore}/100, target: ${targetAgent}`);
@@ -5280,6 +5392,16 @@ ${truncate(skill.content, LIMITS.SKILL_CHARS)}`
5280
5392
  (${existing.claudeSkills.length - LIMITS.SKILLS_MAX} more skills omitted)`);
5281
5393
  }
5282
5394
  }
5395
+ if (existing.claudeRules?.length) {
5396
+ parts.push("\n--- Existing Claude Rules ---");
5397
+ for (const rule of existing.claudeRules.slice(0, LIMITS.RULES_MAX)) {
5398
+ parts.push(
5399
+ `
5400
+ [.claude/rules/${rule.filename}]
5401
+ ${truncate(rule.content, LIMITS.SKILL_CHARS)}`
5402
+ );
5403
+ }
5404
+ }
5283
5405
  if (existing.cursorrules)
5284
5406
  parts.push(
5285
5407
  `
@@ -5327,6 +5449,13 @@ ${existing.personalLearnings}`
5327
5449
  Project dependencies (${allDeps.length}):`);
5328
5450
  parts.push(allDeps.join(", "));
5329
5451
  }
5452
+ if (existing.includableDocs?.length) {
5453
+ parts.push("\n--- Existing Documentation Files (use @include) ---");
5454
+ parts.push("These files exist and can be referenced in CLAUDE.md using @./path:");
5455
+ for (const doc of existing.includableDocs) {
5456
+ parts.push(`- ${doc}`);
5457
+ }
5458
+ }
5330
5459
  if (prompt) parts.push(`
5331
5460
  User instructions: ${prompt}`);
5332
5461
  if (fingerprint.codeAnalysis) {
@@ -5390,18 +5519,29 @@ function writeClaudeConfig(config) {
5390
5519
  appendSyncBlock(appendLearningsBlock(appendPreCommitBlock(config.claudeMd)))
5391
5520
  );
5392
5521
  written.push("CLAUDE.md");
5522
+ if (config.rules?.length) {
5523
+ const rulesDir = path12.join(".claude", "rules");
5524
+ if (!fs12.existsSync(rulesDir)) fs12.mkdirSync(rulesDir, { recursive: true });
5525
+ for (const rule of config.rules) {
5526
+ const rulePath = path12.join(rulesDir, rule.filename);
5527
+ fs12.writeFileSync(rulePath, rule.content);
5528
+ written.push(rulePath);
5529
+ }
5530
+ }
5393
5531
  if (config.skills?.length) {
5394
5532
  for (const skill of config.skills) {
5395
5533
  const skillDir = path12.join(".claude", "skills", skill.name);
5396
5534
  if (!fs12.existsSync(skillDir)) fs12.mkdirSync(skillDir, { recursive: true });
5397
5535
  const skillPath = path12.join(skillDir, "SKILL.md");
5398
- const frontmatter = [
5399
- "---",
5400
- `name: ${skill.name}`,
5401
- `description: ${skill.description}`,
5402
- "---",
5403
- ""
5404
- ].join("\n");
5536
+ const frontmatterLines = ["---", `name: ${skill.name}`, `description: ${skill.description}`];
5537
+ if (skill.paths?.length) {
5538
+ frontmatterLines.push("paths:");
5539
+ for (const p of skill.paths) {
5540
+ frontmatterLines.push(` - ${p}`);
5541
+ }
5542
+ }
5543
+ frontmatterLines.push("---", "");
5544
+ const frontmatter = frontmatterLines.join("\n");
5405
5545
  fs12.writeFileSync(skillPath, frontmatter + skill.content);
5406
5546
  written.push(skillPath);
5407
5547
  }
@@ -5688,6 +5828,9 @@ function getFilesToWrite(setup) {
5688
5828
  if (setup.targetAgent.includes("claude") && setup.claude) {
5689
5829
  files.push("CLAUDE.md");
5690
5830
  if (setup.claude.mcpServers) files.push(".mcp.json");
5831
+ if (setup.claude.rules) {
5832
+ for (const r of setup.claude.rules) files.push(`.claude/rules/${r.filename}`);
5833
+ }
5691
5834
  if (setup.claude.skills) {
5692
5835
  for (const s of setup.claude.skills) {
5693
5836
  files.push(`.claude/skills/${s.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}/SKILL.md`);
@@ -6230,6 +6373,7 @@ var POINTS_SKILLS_BONUS_PER_EXTRA = 1;
6230
6373
  var POINTS_SKILLS_BONUS_CAP = 2;
6231
6374
  var POINTS_CURSOR_MDC_RULES = 3;
6232
6375
  var POINTS_MCP_SERVERS = 3;
6376
+ var POINTS_CLAUDE_RULES = 3;
6233
6377
  var POINTS_CROSS_PLATFORM_PARITY = 2;
6234
6378
  var POINTS_EXECUTABLE_CONTENT = 8;
6235
6379
  var POINTS_CONCISE_CONFIG = 6;
@@ -6299,7 +6443,11 @@ var SECRET_PLACEHOLDER_PATTERNS = [
6299
6443
  /<[^>]+>/
6300
6444
  ];
6301
6445
  var CURSOR_ONLY_CHECKS = /* @__PURE__ */ new Set(["cursor_rules_exist", "cursor_mdc_rules"]);
6302
- var CLAUDE_ONLY_CHECKS = /* @__PURE__ */ new Set(["claude_md_exists", "claude_md_freshness"]);
6446
+ var CLAUDE_ONLY_CHECKS = /* @__PURE__ */ new Set([
6447
+ "claude_md_exists",
6448
+ "claude_md_freshness",
6449
+ "claude_rules_exist"
6450
+ ]);
6303
6451
  var BOTH_ONLY_CHECKS = /* @__PURE__ */ new Set(["cross_platform_parity", "no_duplicate_content"]);
6304
6452
  var CODEX_ONLY_CHECKS = /* @__PURE__ */ new Set(["codex_agents_md_exists"]);
6305
6453
  var COPILOT_ONLY_CHECKS = /* @__PURE__ */ new Set(["copilot_instructions_exists"]);
@@ -6368,6 +6516,32 @@ function checkExistence(dir) {
6368
6516
  instruction: "Create CLAUDE.md with project context, commands, architecture, and conventions."
6369
6517
  }
6370
6518
  });
6519
+ const claudeRulesDir = join3(dir, ".claude", "rules");
6520
+ let claudeRuleFiles = [];
6521
+ if (existsSync3(claudeRulesDir)) {
6522
+ try {
6523
+ claudeRuleFiles = readdirSync2(claudeRulesDir).filter(
6524
+ (f) => f.endsWith(".md") && !f.startsWith(CALIBER_MANAGED_PREFIX)
6525
+ );
6526
+ } catch {
6527
+ }
6528
+ }
6529
+ const hasClaudeRules = claudeRuleFiles.length > 0;
6530
+ checks.push({
6531
+ id: "claude_rules_exist",
6532
+ name: "Claude rules exist (.claude/rules/)",
6533
+ category: "existence",
6534
+ maxPoints: POINTS_CLAUDE_RULES,
6535
+ earnedPoints: hasClaudeRules ? POINTS_CLAUDE_RULES : 0,
6536
+ passed: hasClaudeRules,
6537
+ detail: hasClaudeRules ? `${claudeRuleFiles.length} rule${claudeRuleFiles.length === 1 ? "" : "s"} found` : "No .claude/rules/*.md files",
6538
+ suggestion: hasClaudeRules ? void 0 : "Add .claude/rules/*.md with path-scoped conventions for better context efficiency",
6539
+ fix: hasClaudeRules ? void 0 : {
6540
+ action: "create_file",
6541
+ data: { file: ".claude/rules/" },
6542
+ instruction: "Create .claude/rules/ with path-scoped markdown rules (e.g., testing-patterns.md, api-conventions.md)."
6543
+ }
6544
+ });
6371
6545
  const hasCursorrules = existsSync3(join3(dir, ".cursorrules"));
6372
6546
  const cursorRulesDir = existsSync3(join3(dir, ".cursor", "rules"));
6373
6547
  const cursorRulesExist = hasCursorrules || cursorRulesDir;
@@ -7626,8 +7800,8 @@ function trackInitSkillsSearch(searched, installedCount) {
7626
7800
  function trackInitScoreRegression(oldScore, newScore) {
7627
7801
  trackEvent("init_score_regression", { old_score: oldScore, new_score: newScore });
7628
7802
  }
7629
- function trackInitCompleted(path43, score) {
7630
- trackEvent("init_completed", { path: path43, score });
7803
+ function trackInitCompleted(path44, score) {
7804
+ trackEvent("init_completed", { path: path44, score });
7631
7805
  }
7632
7806
  function trackRegenerateCompleted(action, durationMs) {
7633
7807
  trackEvent("regenerate_completed", { action, duration_ms: durationMs });
@@ -8640,11 +8814,11 @@ async function scoreAndRefine(setup, dir, sessionHistory, callbacks, options) {
8640
8814
  const maxIterations = options?.thorough ? 3 : MAX_REFINE_ITERATIONS;
8641
8815
  const minPoints = options?.thorough ? 1 : MIN_POINTS_TO_REFINE;
8642
8816
  const existsCache = /* @__PURE__ */ new Map();
8643
- const cachedExists = (path43) => {
8644
- const cached = existsCache.get(path43);
8817
+ const cachedExists = (path44) => {
8818
+ const cached = existsCache.get(path44);
8645
8819
  if (cached !== void 0) return cached;
8646
- const result = existsSync9(path43);
8647
- existsCache.set(path43, result);
8820
+ const result = existsSync9(path44);
8821
+ existsCache.set(path44, result);
8648
8822
  return result;
8649
8823
  };
8650
8824
  const projectStructure = collectProjectStructure(dir);
@@ -10056,6 +10230,8 @@ async function initCommand(options) {
10056
10230
  }
10057
10231
  installStopHook();
10058
10232
  console.log(` ${chalk14.green("\u2713")} Onboarding hook \u2014 nudges new team members to set up`);
10233
+ installSessionStartHook();
10234
+ console.log(` ${chalk14.green("\u2713")} Freshness hook \u2014 warns when configs are stale`);
10059
10235
  const { ensureBuiltinSkills: ensureBuiltinSkills2 } = await Promise.resolve().then(() => (init_builtin_skills(), builtin_skills_exports));
10060
10236
  for (const agent of targetAgent) {
10061
10237
  if (agent === "claude" && !fs34.existsSync(".claude"))
@@ -11051,8 +11227,8 @@ async function scoreCommand(options) {
11051
11227
  }
11052
11228
 
11053
11229
  // src/commands/refresh.ts
11054
- import fs40 from "fs";
11055
- import path33 from "path";
11230
+ import fs41 from "fs";
11231
+ import path34 from "path";
11056
11232
  import chalk19 from "chalk";
11057
11233
  import ora6 from "ora";
11058
11234
 
@@ -11139,52 +11315,91 @@ function collectDiff(lastSha) {
11139
11315
  const summary = parts.join(", ") || "no changes";
11140
11316
  return { hasChanges, committedDiff, stagedDiff, unstagedDiff, changedFiles, summary };
11141
11317
  }
11318
+ function scopeDiffToDir(diff, dir, allConfigDirs) {
11319
+ if (dir === ".") {
11320
+ const otherDirs = allConfigDirs.filter((d) => d !== ".");
11321
+ if (otherDirs.length === 0) return diff;
11322
+ const changedFiles2 = diff.changedFiles.filter(
11323
+ (f) => !otherDirs.some((d) => f.startsWith(`${d}/`))
11324
+ );
11325
+ const hasChanges2 = changedFiles2.length > 0;
11326
+ return {
11327
+ ...diff,
11328
+ changedFiles: changedFiles2,
11329
+ hasChanges: hasChanges2,
11330
+ summary: hasChanges2 ? `${changedFiles2.length} files changed` : "no changes"
11331
+ };
11332
+ }
11333
+ const prefix = `${dir}/`;
11334
+ const changedFiles = diff.changedFiles.filter((f) => f.startsWith(prefix)).map((f) => f.slice(prefix.length));
11335
+ const hasChanges = changedFiles.length > 0;
11336
+ return {
11337
+ ...diff,
11338
+ changedFiles,
11339
+ hasChanges,
11340
+ summary: hasChanges ? `${changedFiles.length} files changed` : "no changes"
11341
+ };
11342
+ }
11142
11343
 
11143
11344
  // src/writers/refresh.ts
11144
11345
  init_pre_commit_block();
11145
11346
  import fs37 from "fs";
11146
11347
  import path30 from "path";
11147
- function writeRefreshDocs(docs) {
11348
+ function writeFileGroup(groupDir, files) {
11349
+ fs37.mkdirSync(groupDir, { recursive: true });
11350
+ return files.map((file) => {
11351
+ const filePath = path30.join(groupDir, file.filename);
11352
+ fs37.writeFileSync(filePath, file.content);
11353
+ return filePath.replace(/\\/g, "/");
11354
+ });
11355
+ }
11356
+ function writeRefreshDocs(docs, dir = ".") {
11148
11357
  const written = [];
11358
+ const p = (relPath) => (dir === "." ? relPath : path30.join(dir, relPath)).replace(/\\/g, "/");
11359
+ const ensureParent = (filePath) => {
11360
+ const parent = path30.dirname(filePath);
11361
+ if (parent !== "." && !fs37.existsSync(parent)) fs37.mkdirSync(parent, { recursive: true });
11362
+ };
11149
11363
  if (docs.agentsMd) {
11150
- fs37.writeFileSync("AGENTS.md", appendManagedBlocks(docs.agentsMd, "codex"));
11151
- written.push("AGENTS.md");
11364
+ const filePath = p("AGENTS.md");
11365
+ ensureParent(filePath);
11366
+ fs37.writeFileSync(filePath, appendManagedBlocks(docs.agentsMd, "codex"));
11367
+ written.push(filePath);
11152
11368
  }
11153
11369
  if (docs.claudeMd) {
11154
- fs37.writeFileSync("CLAUDE.md", appendManagedBlocks(docs.claudeMd));
11155
- written.push("CLAUDE.md");
11370
+ const filePath = p("CLAUDE.md");
11371
+ ensureParent(filePath);
11372
+ fs37.writeFileSync(filePath, appendManagedBlocks(docs.claudeMd));
11373
+ written.push(filePath);
11374
+ }
11375
+ if (docs.claudeRules) {
11376
+ written.push(...writeFileGroup(p(path30.join(".claude", "rules")), docs.claudeRules));
11156
11377
  }
11157
11378
  if (docs.readmeMd) {
11158
- fs37.writeFileSync("README.md", docs.readmeMd);
11159
- written.push("README.md");
11379
+ const filePath = p("README.md");
11380
+ ensureParent(filePath);
11381
+ fs37.writeFileSync(filePath, docs.readmeMd);
11382
+ written.push(filePath);
11160
11383
  }
11161
11384
  if (docs.cursorrules) {
11162
- fs37.writeFileSync(".cursorrules", docs.cursorrules);
11163
- written.push(".cursorrules");
11385
+ const filePath = p(".cursorrules");
11386
+ ensureParent(filePath);
11387
+ fs37.writeFileSync(filePath, docs.cursorrules);
11388
+ written.push(filePath);
11164
11389
  }
11165
11390
  if (docs.cursorRules) {
11166
- const rulesDir = path30.join(".cursor", "rules");
11167
- if (!fs37.existsSync(rulesDir)) fs37.mkdirSync(rulesDir, { recursive: true });
11168
- for (const rule of docs.cursorRules) {
11169
- fs37.writeFileSync(path30.join(rulesDir, rule.filename), rule.content);
11170
- written.push(`.cursor/rules/${rule.filename}`);
11171
- }
11391
+ written.push(...writeFileGroup(p(path30.join(".cursor", "rules")), docs.cursorRules));
11172
11392
  }
11173
11393
  if (docs.copilotInstructions) {
11174
- fs37.mkdirSync(".github", { recursive: true });
11175
- fs37.writeFileSync(
11176
- path30.join(".github", "copilot-instructions.md"),
11177
- appendManagedBlocks(docs.copilotInstructions, "copilot")
11178
- );
11179
- written.push(".github/copilot-instructions.md");
11394
+ const filePath = p(path30.join(".github", "copilot-instructions.md"));
11395
+ ensureParent(filePath);
11396
+ fs37.writeFileSync(filePath, appendManagedBlocks(docs.copilotInstructions, "copilot"));
11397
+ written.push(filePath);
11180
11398
  }
11181
11399
  if (docs.copilotInstructionFiles) {
11182
- const instructionsDir = path30.join(".github", "instructions");
11183
- fs37.mkdirSync(instructionsDir, { recursive: true });
11184
- for (const file of docs.copilotInstructionFiles) {
11185
- fs37.writeFileSync(path30.join(instructionsDir, file.filename), file.content);
11186
- written.push(`.github/instructions/${file.filename}`);
11187
- }
11400
+ written.push(
11401
+ ...writeFileGroup(p(path30.join(".github", "instructions")), docs.copilotInstructionFiles)
11402
+ );
11188
11403
  }
11189
11404
  return written;
11190
11405
  }
@@ -11192,8 +11407,15 @@ function writeRefreshDocs(docs) {
11192
11407
  // src/ai/refresh.ts
11193
11408
  init_config();
11194
11409
  init_pre_commit_block();
11195
- async function refreshDocs(diff, existingDocs, projectContext, learnedSection, sources2) {
11196
- const prompt = buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection, sources2);
11410
+ async function refreshDocs(diff, existingDocs, projectContext, learnedSection, sources2, scope) {
11411
+ const prompt = buildRefreshPrompt(
11412
+ diff,
11413
+ existingDocs,
11414
+ projectContext,
11415
+ learnedSection,
11416
+ sources2,
11417
+ scope
11418
+ );
11197
11419
  const fastModel = getFastModel();
11198
11420
  const raw = await llmCall({
11199
11421
  system: REFRESH_SYSTEM_PROMPT,
@@ -11203,8 +11425,14 @@ async function refreshDocs(diff, existingDocs, projectContext, learnedSection, s
11203
11425
  });
11204
11426
  return parseJsonResponse(raw);
11205
11427
  }
11206
- function buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection, sources2) {
11428
+ function buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection, sources2, scope) {
11207
11429
  const parts = [];
11430
+ if (scope) {
11431
+ parts.push(`You are updating docs for the \`${scope}\` subdirectory of a monorepo.`);
11432
+ parts.push("Only include changes relevant to files under this directory.");
11433
+ parts.push("The changed files list has been filtered to this directory already.");
11434
+ parts.push("Ignore diff content for files outside this directory.\n");
11435
+ }
11208
11436
  parts.push("Update documentation based on the following code changes.\n");
11209
11437
  if (projectContext.packageName) parts.push(`Project: ${projectContext.packageName}`);
11210
11438
  if (projectContext.languages?.length)
@@ -11258,9 +11486,17 @@ Changed files: ${diff.changedFiles.join(", ")}`);
11258
11486
  parts.push(skill.content);
11259
11487
  }
11260
11488
  }
11489
+ if (existingDocs.claudeRules?.length) {
11490
+ for (const rule of existingDocs.claudeRules) {
11491
+ if (rule.filename.startsWith(CALIBER_MANAGED_PREFIX)) continue;
11492
+ parts.push(`
11493
+ [.claude/rules/${rule.filename}]`);
11494
+ parts.push(rule.content);
11495
+ }
11496
+ }
11261
11497
  if (existingDocs.cursorRules?.length) {
11262
11498
  for (const rule of existingDocs.cursorRules) {
11263
- if (rule.filename.startsWith("caliber-")) continue;
11499
+ if (rule.filename.startsWith(CALIBER_MANAGED_PREFIX)) continue;
11264
11500
  parts.push(`
11265
11501
  [.cursor/rules/${rule.filename}]`);
11266
11502
  parts.push(rule.content);
@@ -11277,6 +11513,16 @@ Changed files: ${diff.changedFiles.join(", ")}`);
11277
11513
  parts.push(file.content);
11278
11514
  }
11279
11515
  }
11516
+ if (existingDocs.includableDocs?.length) {
11517
+ parts.push(`
11518
+ --- Existing Documentation Files (use @include) ---`);
11519
+ parts.push(
11520
+ "These files exist in the project and can be referenced in CLAUDE.md using @./path:"
11521
+ );
11522
+ for (const doc of existingDocs.includableDocs) {
11523
+ parts.push(`- ${doc}`);
11524
+ }
11525
+ }
11280
11526
  if (learnedSection) {
11281
11527
  parts.push("\n--- Learned Patterns (from session learning) ---");
11282
11528
  parts.push("Consider these accumulated learnings when deciding what to update:");
@@ -11486,10 +11732,77 @@ function migrateInlineLearnings() {
11486
11732
  init_config();
11487
11733
  init_resolve_caliber();
11488
11734
  init_builtin_skills();
11735
+
11736
+ // src/lib/config-discovery.ts
11737
+ import fs39 from "fs";
11738
+ import path32 from "path";
11739
+ var CONFIG_FILE_MARKERS = [
11740
+ "CLAUDE.md",
11741
+ "AGENTS.md",
11742
+ ".cursorrules",
11743
+ ".github/copilot-instructions.md"
11744
+ ];
11745
+ var CONFIG_DIR_MARKERS = [".cursor/rules", ".github/instructions", ".opencode/skills"];
11746
+ var IGNORE_DIRS3 = /* @__PURE__ */ new Set([
11747
+ "node_modules",
11748
+ ".git",
11749
+ ".next",
11750
+ "dist",
11751
+ "build",
11752
+ ".cache",
11753
+ ".turbo",
11754
+ "coverage",
11755
+ ".caliber",
11756
+ "__pycache__",
11757
+ ".venv",
11758
+ "vendor",
11759
+ "target"
11760
+ ]);
11761
+ var MAX_DEPTH = 4;
11762
+ function hasConfigFiles(dir) {
11763
+ for (const marker of CONFIG_FILE_MARKERS) {
11764
+ if (fs39.existsSync(path32.join(dir, marker))) return true;
11765
+ }
11766
+ for (const marker of CONFIG_DIR_MARKERS) {
11767
+ const markerPath = path32.join(dir, marker);
11768
+ if (fs39.existsSync(markerPath) && fs39.statSync(markerPath).isDirectory()) return true;
11769
+ }
11770
+ return false;
11771
+ }
11772
+ function discoverConfigDirs(rootDir) {
11773
+ const dirs = [];
11774
+ if (hasConfigFiles(rootDir)) {
11775
+ dirs.push(".");
11776
+ }
11777
+ walkForConfigs(rootDir, rootDir, 0, dirs);
11778
+ dirs.sort();
11779
+ return dirs;
11780
+ }
11781
+ function walkForConfigs(baseDir, currentDir, depth, result) {
11782
+ if (depth >= MAX_DEPTH) return;
11783
+ let entries;
11784
+ try {
11785
+ entries = fs39.readdirSync(currentDir, { withFileTypes: true });
11786
+ } catch {
11787
+ return;
11788
+ }
11789
+ for (const entry of entries) {
11790
+ if (!entry.isDirectory()) continue;
11791
+ if (entry.name.startsWith(".") || IGNORE_DIRS3.has(entry.name)) continue;
11792
+ const fullPath = path32.join(currentDir, entry.name);
11793
+ const relPath = path32.relative(baseDir, fullPath).replace(/\\/g, "/");
11794
+ if (hasConfigFiles(fullPath)) {
11795
+ result.push(relPath);
11796
+ }
11797
+ walkForConfigs(baseDir, fullPath, depth + 1, result);
11798
+ }
11799
+ }
11800
+
11801
+ // src/commands/refresh.ts
11489
11802
  function writeRefreshError(error) {
11490
11803
  try {
11491
- if (!fs40.existsSync(CALIBER_DIR)) fs40.mkdirSync(CALIBER_DIR, { recursive: true });
11492
- fs40.writeFileSync(
11804
+ if (!fs41.existsSync(CALIBER_DIR)) fs41.mkdirSync(CALIBER_DIR, { recursive: true });
11805
+ fs41.writeFileSync(
11493
11806
  REFRESH_LAST_ERROR_FILE,
11494
11807
  JSON.stringify(
11495
11808
  {
@@ -11508,15 +11821,15 @@ function writeRefreshError(error) {
11508
11821
  }
11509
11822
  function readRefreshError() {
11510
11823
  try {
11511
- if (!fs40.existsSync(REFRESH_LAST_ERROR_FILE)) return null;
11512
- return JSON.parse(fs40.readFileSync(REFRESH_LAST_ERROR_FILE, "utf-8"));
11824
+ if (!fs41.existsSync(REFRESH_LAST_ERROR_FILE)) return null;
11825
+ return JSON.parse(fs41.readFileSync(REFRESH_LAST_ERROR_FILE, "utf-8"));
11513
11826
  } catch {
11514
11827
  return null;
11515
11828
  }
11516
11829
  }
11517
11830
  function clearRefreshError() {
11518
11831
  try {
11519
- if (fs40.existsSync(REFRESH_LAST_ERROR_FILE)) fs40.unlinkSync(REFRESH_LAST_ERROR_FILE);
11832
+ if (fs41.existsSync(REFRESH_LAST_ERROR_FILE)) fs41.unlinkSync(REFRESH_LAST_ERROR_FILE);
11520
11833
  } catch {
11521
11834
  }
11522
11835
  }
@@ -11536,11 +11849,11 @@ function log2(quiet, ...args) {
11536
11849
  function discoverGitRepos(parentDir) {
11537
11850
  const repos = [];
11538
11851
  try {
11539
- const entries = fs40.readdirSync(parentDir, { withFileTypes: true });
11852
+ const entries = fs41.readdirSync(parentDir, { withFileTypes: true });
11540
11853
  for (const entry of entries) {
11541
11854
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
11542
- const childPath = path33.join(parentDir, entry.name);
11543
- if (fs40.existsSync(path33.join(childPath, ".git"))) {
11855
+ const childPath = path34.join(parentDir, entry.name);
11856
+ if (fs41.existsSync(path34.join(childPath, ".git"))) {
11544
11857
  repos.push(childPath);
11545
11858
  }
11546
11859
  }
@@ -11548,51 +11861,37 @@ function discoverGitRepos(parentDir) {
11548
11861
  }
11549
11862
  return repos.sort();
11550
11863
  }
11551
- function collectFilesToWrite(updatedDocs) {
11864
+ function collectFilesToWrite(updatedDocs, dir = ".") {
11552
11865
  const files = [];
11553
- if (updatedDocs.agentsMd) files.push("AGENTS.md");
11554
- if (updatedDocs.claudeMd) files.push("CLAUDE.md");
11555
- if (updatedDocs.readmeMd) files.push("README.md");
11556
- if (updatedDocs.cursorrules) files.push(".cursorrules");
11866
+ const p = (relPath) => (dir === "." ? relPath : path34.join(dir, relPath)).replace(/\\/g, "/");
11867
+ if (updatedDocs.agentsMd) files.push(p("AGENTS.md"));
11868
+ if (updatedDocs.claudeMd) files.push(p("CLAUDE.md"));
11869
+ if (Array.isArray(updatedDocs.claudeRules)) {
11870
+ for (const r of updatedDocs.claudeRules)
11871
+ files.push(p(`.claude/rules/${r.filename}`));
11872
+ }
11873
+ if (updatedDocs.readmeMd) files.push(p("README.md"));
11874
+ if (updatedDocs.cursorrules) files.push(p(".cursorrules"));
11557
11875
  if (Array.isArray(updatedDocs.cursorRules)) {
11558
11876
  for (const r of updatedDocs.cursorRules)
11559
- files.push(`.cursor/rules/${r.filename}`);
11877
+ files.push(p(`.cursor/rules/${r.filename}`));
11560
11878
  }
11561
- if (updatedDocs.copilotInstructions) files.push(".github/copilot-instructions.md");
11879
+ if (updatedDocs.copilotInstructions) files.push(p(".github/copilot-instructions.md"));
11562
11880
  if (Array.isArray(updatedDocs.copilotInstructionFiles)) {
11563
11881
  for (const f of updatedDocs.copilotInstructionFiles)
11564
- files.push(`.github/instructions/${f.filename}`);
11882
+ files.push(p(`.github/instructions/${f.filename}`));
11565
11883
  }
11566
11884
  return files;
11567
11885
  }
11568
11886
  var REFRESH_COOLDOWN_MS = 3e4;
11569
- async function refreshSingleRepo(repoDir, options) {
11887
+ async function refreshDir(repoDir, dir, diff, options) {
11570
11888
  const quiet = !!options.quiet;
11571
11889
  const prefix = options.label ? `${chalk19.bold(options.label)} ` : "";
11572
- const state = readState();
11573
- const lastSha = state?.lastRefreshSha ?? null;
11574
- const currentSha = getCurrentHeadSha();
11575
- if (state?.lastRefreshTimestamp && lastSha && currentSha === lastSha) {
11576
- const elapsed = Date.now() - new Date(state.lastRefreshTimestamp).getTime();
11577
- if (elapsed < REFRESH_COOLDOWN_MS && elapsed > 0) {
11578
- log2(
11579
- quiet,
11580
- chalk19.dim(`${prefix}Skipped \u2014 last refresh was ${Math.round(elapsed / 1e3)}s ago.`)
11581
- );
11582
- return;
11583
- }
11584
- }
11585
- const diff = collectDiff(lastSha);
11586
- if (!diff.hasChanges) {
11587
- if (currentSha) {
11588
- writeState({ lastRefreshSha: currentSha, lastRefreshTimestamp: (/* @__PURE__ */ new Date()).toISOString() });
11589
- }
11590
- log2(quiet, chalk19.dim(`${prefix}No changes since last refresh.`));
11591
- return;
11592
- }
11890
+ const absDir = dir === "." ? repoDir : path34.resolve(repoDir, dir);
11891
+ const scope = dir === "." ? void 0 : dir;
11593
11892
  const spinner = quiet ? null : ora6(`${prefix}Analyzing changes...`).start();
11594
11893
  const learnedSection = readLearnedSection();
11595
- const fingerprint = await collectFingerprint(repoDir);
11894
+ const fingerprint = await collectFingerprint(absDir);
11596
11895
  const existingDocs = fingerprint.existingConfigs;
11597
11896
  const projectContext = {
11598
11897
  languages: fingerprint.languages,
@@ -11600,8 +11899,8 @@ async function refreshSingleRepo(repoDir, options) {
11600
11899
  packageName: fingerprint.packageName,
11601
11900
  fileTree: fingerprint.fileTree
11602
11901
  };
11603
- const workspaces = getDetectedWorkspaces(repoDir);
11604
- const sources2 = resolveAllSources(repoDir, [], workspaces);
11902
+ const workspaces = getDetectedWorkspaces(absDir);
11903
+ const sources2 = resolveAllSources(absDir, [], workspaces);
11605
11904
  const diffPayload = {
11606
11905
  committed: diff.committedDiff,
11607
11906
  staged: diff.stagedDiff,
@@ -11617,7 +11916,8 @@ async function refreshSingleRepo(repoDir, options) {
11617
11916
  existingDocs,
11618
11917
  projectContext,
11619
11918
  learnedSection,
11620
- sourcesPayload
11919
+ sourcesPayload,
11920
+ scope
11621
11921
  );
11622
11922
  } catch (firstErr) {
11623
11923
  const isTransient = firstErr instanceof Error && TRANSIENT_ERRORS.some((e) => firstErr.message.toLowerCase().includes(e.toLowerCase()));
@@ -11628,7 +11928,8 @@ async function refreshSingleRepo(repoDir, options) {
11628
11928
  existingDocs,
11629
11929
  projectContext,
11630
11930
  learnedSection,
11631
- sourcesPayload
11931
+ sourcesPayload,
11932
+ scope
11632
11933
  );
11633
11934
  } catch {
11634
11935
  spinner?.fail(`${prefix}Refresh failed after retry`);
@@ -11637,10 +11938,7 @@ async function refreshSingleRepo(repoDir, options) {
11637
11938
  }
11638
11939
  if (!response.docsUpdated || response.docsUpdated.length === 0) {
11639
11940
  spinner?.succeed(`${prefix}No doc updates needed`);
11640
- if (currentSha) {
11641
- writeState({ lastRefreshSha: currentSha, lastRefreshTimestamp: (/* @__PURE__ */ new Date()).toISOString() });
11642
- }
11643
- return;
11941
+ return { written: [] };
11644
11942
  }
11645
11943
  if (options.dryRun) {
11646
11944
  spinner?.info(`${prefix}Dry run \u2014 would update:`);
@@ -11651,49 +11949,53 @@ async function refreshSingleRepo(repoDir, options) {
11651
11949
  console.log(chalk19.dim(`
11652
11950
  ${response.changesSummary}`));
11653
11951
  }
11654
- return;
11952
+ return { written: [] };
11655
11953
  }
11656
- const targetAgent = state?.targetAgent ?? detectTargetAgent(repoDir);
11657
- const preScore = computeLocalScore(repoDir, targetAgent);
11658
- const allFilesToWrite = collectFilesToWrite(response.updatedDocs);
11954
+ const allFilesToWrite = collectFilesToWrite(response.updatedDocs, dir);
11659
11955
  const preRefreshContents = /* @__PURE__ */ new Map();
11660
11956
  for (const filePath of allFilesToWrite) {
11661
- const fullPath = path33.resolve(repoDir, filePath);
11957
+ const fullPath = path34.resolve(repoDir, filePath);
11662
11958
  try {
11663
- preRefreshContents.set(filePath, fs40.readFileSync(fullPath, "utf-8"));
11959
+ preRefreshContents.set(filePath, fs41.readFileSync(fullPath, "utf-8"));
11664
11960
  } catch {
11665
11961
  preRefreshContents.set(filePath, null);
11666
11962
  }
11667
11963
  }
11668
- const written = writeRefreshDocs(response.updatedDocs);
11964
+ const state = readState();
11965
+ const targetAgent = state?.targetAgent ?? detectTargetAgent(repoDir);
11966
+ const runQualityGate = dir === ".";
11967
+ const preScore = runQualityGate ? computeLocalScore(absDir, targetAgent) : null;
11968
+ const written = writeRefreshDocs(response.updatedDocs, dir);
11669
11969
  const trigger = quiet ? "hook" : "manual";
11670
11970
  trackRefreshCompleted(written.length, Date.now(), trigger);
11671
- const postScore = computeLocalScore(repoDir, targetAgent);
11672
- if (postScore.score < preScore.score) {
11673
- for (const [filePath, content] of preRefreshContents) {
11674
- const fullPath = path33.resolve(repoDir, filePath);
11675
- if (content === null) {
11676
- try {
11677
- fs40.unlinkSync(fullPath);
11678
- } catch {
11971
+ if (runQualityGate && preScore) {
11972
+ const postScore = computeLocalScore(absDir, targetAgent);
11973
+ if (postScore.score < preScore.score) {
11974
+ for (const [filePath, content] of preRefreshContents) {
11975
+ const fullPath = path34.resolve(repoDir, filePath);
11976
+ if (content === null) {
11977
+ try {
11978
+ fs41.unlinkSync(fullPath);
11979
+ } catch {
11980
+ }
11981
+ } else {
11982
+ fs41.writeFileSync(fullPath, content);
11679
11983
  }
11680
- } else {
11681
- fs40.writeFileSync(fullPath, content);
11682
11984
  }
11985
+ spinner?.warn(
11986
+ `${prefix}Refresh reverted \u2014 score would drop from ${preScore.score} to ${postScore.score}`
11987
+ );
11988
+ log2(quiet, chalk19.dim(` Config quality gate prevented a regression. No files were changed.`));
11989
+ return { written: [] };
11683
11990
  }
11684
- spinner?.warn(
11685
- `${prefix}Refresh reverted \u2014 score would drop from ${preScore.score} to ${postScore.score}`
11686
- );
11687
- log2(quiet, chalk19.dim(` Config quality gate prevented a regression. No files were changed.`));
11688
- if (currentSha) {
11689
- writeState({ lastRefreshSha: currentSha, lastRefreshTimestamp: (/* @__PURE__ */ new Date()).toISOString() });
11690
- }
11691
- return;
11991
+ recordScore(postScore, "refresh");
11692
11992
  }
11693
- recordScore(postScore, "refresh");
11694
11993
  spinner?.succeed(`${prefix}Updated ${written.length} doc${written.length === 1 ? "" : "s"}`);
11695
11994
  const fileChangesMap = new Map(
11696
- (response.fileChanges || []).map((fc) => [fc.file, fc.description])
11995
+ (response.fileChanges || []).map((fc) => [
11996
+ fc.file,
11997
+ fc.description
11998
+ ])
11697
11999
  );
11698
12000
  for (const file of written) {
11699
12001
  const desc = fileChangesMap.get(file);
@@ -11709,6 +12011,59 @@ async function refreshSingleRepo(repoDir, options) {
11709
12011
  log2(quiet, chalk19.dim(`
11710
12012
  ${response.changesSummary}`));
11711
12013
  }
12014
+ return { written };
12015
+ }
12016
+ async function refreshSingleRepo(repoDir, options) {
12017
+ const quiet = !!options.quiet;
12018
+ const prefix = options.label ? `${chalk19.bold(options.label)} ` : "";
12019
+ const state = readState();
12020
+ const lastSha = state?.lastRefreshSha ?? null;
12021
+ const currentSha = getCurrentHeadSha();
12022
+ if (state?.lastRefreshTimestamp && lastSha && currentSha === lastSha) {
12023
+ const elapsed = Date.now() - new Date(state.lastRefreshTimestamp).getTime();
12024
+ if (elapsed < REFRESH_COOLDOWN_MS && elapsed > 0) {
12025
+ log2(
12026
+ quiet,
12027
+ chalk19.dim(`${prefix}Skipped \u2014 last refresh was ${Math.round(elapsed / 1e3)}s ago.`)
12028
+ );
12029
+ return;
12030
+ }
12031
+ }
12032
+ const diff = collectDiff(lastSha);
12033
+ if (!diff.hasChanges) {
12034
+ if (currentSha) {
12035
+ writeState({ lastRefreshSha: currentSha, lastRefreshTimestamp: (/* @__PURE__ */ new Date()).toISOString() });
12036
+ }
12037
+ log2(quiet, chalk19.dim(`${prefix}No changes since last refresh.`));
12038
+ return;
12039
+ }
12040
+ const configDirs = discoverConfigDirs(repoDir);
12041
+ if (configDirs.length <= 1) {
12042
+ await refreshDir(repoDir, ".", diff, options);
12043
+ } else {
12044
+ log2(quiet, chalk19.dim(`${prefix}Found configs in ${configDirs.length} directories
12045
+ `));
12046
+ let hadFailure = false;
12047
+ for (const dir of configDirs) {
12048
+ const scopedDiff = scopeDiffToDir(diff, dir, configDirs);
12049
+ if (!scopedDiff.hasChanges) continue;
12050
+ const dirLabel = dir === "." ? "root" : dir;
12051
+ try {
12052
+ await refreshDir(repoDir, dir, scopedDiff, { ...options, label: dirLabel });
12053
+ } catch (err) {
12054
+ hadFailure = true;
12055
+ log2(
12056
+ quiet,
12057
+ chalk19.yellow(
12058
+ ` ${dirLabel}: refresh failed \u2014 ${err instanceof Error ? err.message : "unknown error"}`
12059
+ )
12060
+ );
12061
+ }
12062
+ }
12063
+ if (hadFailure) {
12064
+ return;
12065
+ }
12066
+ }
11712
12067
  const builtinWritten = ensureBuiltinSkills();
11713
12068
  for (const file of builtinWritten) {
11714
12069
  log2(quiet, ` ${chalk19.green("\u2713")} ${file} ${chalk19.dim("(built-in)")}`);
@@ -11765,7 +12120,7 @@ async function refreshCommand(options) {
11765
12120
  `));
11766
12121
  const originalDir = process.cwd();
11767
12122
  for (const repo of repos) {
11768
- const repoName = path33.basename(repo);
12123
+ const repoName = path34.basename(repo);
11769
12124
  try {
11770
12125
  process.chdir(repo);
11771
12126
  await refreshSingleRepo(repo, { ...options, label: repoName });
@@ -11793,7 +12148,7 @@ async function refreshCommand(options) {
11793
12148
 
11794
12149
  // src/commands/hooks.ts
11795
12150
  import chalk20 from "chalk";
11796
- import fs41 from "fs";
12151
+ import fs42 from "fs";
11797
12152
  var HOOKS = [
11798
12153
  {
11799
12154
  id: "session-end",
@@ -11810,6 +12165,22 @@ var HOOKS = [
11810
12165
  isInstalled: isPreCommitHookInstalled,
11811
12166
  install: installPreCommitHook,
11812
12167
  remove: removePreCommitHook
12168
+ },
12169
+ {
12170
+ id: "session-start",
12171
+ label: "Claude Code SessionStart",
12172
+ description: "Check config freshness when a session starts",
12173
+ isInstalled: isSessionStartHookInstalled,
12174
+ install: installSessionStartHook,
12175
+ remove: removeSessionStartHook
12176
+ },
12177
+ {
12178
+ id: "notification",
12179
+ label: "Claude Code Notification",
12180
+ description: "Warn when agent configs are stale (>15 commits behind)",
12181
+ isInstalled: isNotificationHookInstalled,
12182
+ install: installNotificationHook,
12183
+ remove: removeNotificationHook
11813
12184
  }
11814
12185
  ];
11815
12186
  function printStatus() {
@@ -11825,8 +12196,12 @@ function printStatus() {
11825
12196
  }
11826
12197
  async function hooksCommand(options) {
11827
12198
  if (!options.install && !options.remove) {
11828
- console.log(chalk20.dim("\n Note: caliber now adds refresh instructions directly to config files."));
11829
- console.log(chalk20.dim(" These hooks are available for non-agent workflows (manual commits).\n"));
12199
+ console.log(
12200
+ chalk20.dim("\n Note: caliber now adds refresh instructions directly to config files.")
12201
+ );
12202
+ console.log(
12203
+ chalk20.dim(" These hooks are available for non-agent workflows (manual commits).\n")
12204
+ );
11830
12205
  }
11831
12206
  if (options.install) {
11832
12207
  for (const hook of HOOKS) {
@@ -11837,11 +12212,11 @@ async function hooksCommand(options) {
11837
12212
  console.log(chalk20.green(" \u2713") + ` ${hook.label} enabled`);
11838
12213
  }
11839
12214
  }
11840
- if (fs41.existsSync(".claude")) {
12215
+ if (fs42.existsSync(".claude")) {
11841
12216
  const r = installLearningHooks();
11842
12217
  if (r.installed) console.log(chalk20.green(" \u2713") + " Claude Code learning hooks enabled");
11843
12218
  }
11844
- if (fs41.existsSync(".cursor")) {
12219
+ if (fs42.existsSync(".cursor")) {
11845
12220
  const r = installCursorLearningHooks();
11846
12221
  if (r.installed) console.log(chalk20.green(" \u2713") + " Cursor learning hooks enabled");
11847
12222
  }
@@ -12009,8 +12384,8 @@ async function configCommand() {
12009
12384
  }
12010
12385
 
12011
12386
  // src/commands/learn.ts
12012
- import fs45 from "fs";
12013
- import path37 from "path";
12387
+ import fs46 from "fs";
12388
+ import path38 from "path";
12014
12389
  import chalk23 from "chalk";
12015
12390
 
12016
12391
  // src/learner/stdin.ts
@@ -12041,8 +12416,8 @@ function readStdin() {
12041
12416
  }
12042
12417
 
12043
12418
  // src/learner/storage.ts
12044
- import fs42 from "fs";
12045
- import path34 from "path";
12419
+ import fs43 from "fs";
12420
+ import path35 from "path";
12046
12421
  var MAX_RESPONSE_LENGTH = 2e3;
12047
12422
  var DEFAULT_STATE = {
12048
12423
  sessionId: null,
@@ -12051,15 +12426,15 @@ var DEFAULT_STATE = {
12051
12426
  lastAnalysisEventCount: 0
12052
12427
  };
12053
12428
  function ensureLearningDir() {
12054
- if (!fs42.existsSync(getLearningDir())) {
12055
- fs42.mkdirSync(getLearningDir(), { recursive: true });
12429
+ if (!fs43.existsSync(getLearningDir())) {
12430
+ fs43.mkdirSync(getLearningDir(), { recursive: true });
12056
12431
  }
12057
12432
  }
12058
12433
  function sessionFilePath() {
12059
- return path34.join(getLearningDir(), LEARNING_SESSION_FILE);
12434
+ return path35.join(getLearningDir(), LEARNING_SESSION_FILE);
12060
12435
  }
12061
12436
  function stateFilePath() {
12062
- return path34.join(getLearningDir(), LEARNING_STATE_FILE);
12437
+ return path35.join(getLearningDir(), LEARNING_STATE_FILE);
12063
12438
  }
12064
12439
  function truncateResponse(response) {
12065
12440
  const str = JSON.stringify(response);
@@ -12069,10 +12444,10 @@ function truncateResponse(response) {
12069
12444
  function trimSessionFileIfNeeded(filePath) {
12070
12445
  const state = readState2();
12071
12446
  if (state.eventCount + 1 > LEARNING_MAX_EVENTS) {
12072
- const lines = fs42.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
12447
+ const lines = fs43.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
12073
12448
  if (lines.length > LEARNING_MAX_EVENTS) {
12074
12449
  const kept = lines.slice(lines.length - LEARNING_MAX_EVENTS);
12075
- fs42.writeFileSync(filePath, kept.join("\n") + "\n");
12450
+ fs43.writeFileSync(filePath, kept.join("\n") + "\n");
12076
12451
  }
12077
12452
  }
12078
12453
  }
@@ -12080,19 +12455,19 @@ function appendEvent(event) {
12080
12455
  ensureLearningDir();
12081
12456
  const truncated = { ...event, tool_response: truncateResponse(event.tool_response) };
12082
12457
  const filePath = sessionFilePath();
12083
- fs42.appendFileSync(filePath, JSON.stringify(truncated) + "\n");
12458
+ fs43.appendFileSync(filePath, JSON.stringify(truncated) + "\n");
12084
12459
  trimSessionFileIfNeeded(filePath);
12085
12460
  }
12086
12461
  function appendPromptEvent(event) {
12087
12462
  ensureLearningDir();
12088
12463
  const filePath = sessionFilePath();
12089
- fs42.appendFileSync(filePath, JSON.stringify(event) + "\n");
12464
+ fs43.appendFileSync(filePath, JSON.stringify(event) + "\n");
12090
12465
  trimSessionFileIfNeeded(filePath);
12091
12466
  }
12092
12467
  function readAllEvents() {
12093
12468
  const filePath = sessionFilePath();
12094
- if (!fs42.existsSync(filePath)) return [];
12095
- const lines = fs42.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
12469
+ if (!fs43.existsSync(filePath)) return [];
12470
+ const lines = fs43.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
12096
12471
  const events = [];
12097
12472
  for (const line of lines) {
12098
12473
  try {
@@ -12104,26 +12479,26 @@ function readAllEvents() {
12104
12479
  }
12105
12480
  function getEventCount() {
12106
12481
  const filePath = sessionFilePath();
12107
- if (!fs42.existsSync(filePath)) return 0;
12108
- const content = fs42.readFileSync(filePath, "utf-8");
12482
+ if (!fs43.existsSync(filePath)) return 0;
12483
+ const content = fs43.readFileSync(filePath, "utf-8");
12109
12484
  return content.split("\n").filter(Boolean).length;
12110
12485
  }
12111
12486
  function clearSession() {
12112
12487
  const filePath = sessionFilePath();
12113
- if (fs42.existsSync(filePath)) fs42.unlinkSync(filePath);
12488
+ if (fs43.existsSync(filePath)) fs43.unlinkSync(filePath);
12114
12489
  }
12115
12490
  function readState2() {
12116
12491
  const filePath = stateFilePath();
12117
- if (!fs42.existsSync(filePath)) return { ...DEFAULT_STATE };
12492
+ if (!fs43.existsSync(filePath)) return { ...DEFAULT_STATE };
12118
12493
  try {
12119
- return JSON.parse(fs42.readFileSync(filePath, "utf-8"));
12494
+ return JSON.parse(fs43.readFileSync(filePath, "utf-8"));
12120
12495
  } catch {
12121
12496
  return { ...DEFAULT_STATE };
12122
12497
  }
12123
12498
  }
12124
12499
  function writeState2(state) {
12125
12500
  ensureLearningDir();
12126
- fs42.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
12501
+ fs43.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
12127
12502
  }
12128
12503
  function resetState() {
12129
12504
  writeState2({ ...DEFAULT_STATE });
@@ -12131,16 +12506,16 @@ function resetState() {
12131
12506
  var LOCK_FILE = "finalize.lock";
12132
12507
  var LOCK_STALE_MS = 5 * 60 * 1e3;
12133
12508
  function lockFilePath() {
12134
- return path34.join(getLearningDir(), LOCK_FILE);
12509
+ return path35.join(getLearningDir(), LOCK_FILE);
12135
12510
  }
12136
12511
  function acquireFinalizeLock() {
12137
12512
  ensureLearningDir();
12138
12513
  const lockPath = lockFilePath();
12139
- if (fs42.existsSync(lockPath)) {
12514
+ if (fs43.existsSync(lockPath)) {
12140
12515
  try {
12141
- const stat = fs42.statSync(lockPath);
12516
+ const stat = fs43.statSync(lockPath);
12142
12517
  if (Date.now() - stat.mtimeMs < LOCK_STALE_MS) {
12143
- const pid = parseInt(fs42.readFileSync(lockPath, "utf-8").trim(), 10);
12518
+ const pid = parseInt(fs43.readFileSync(lockPath, "utf-8").trim(), 10);
12144
12519
  if (!isNaN(pid) && isProcessAlive(pid)) {
12145
12520
  return false;
12146
12521
  }
@@ -12148,12 +12523,12 @@ function acquireFinalizeLock() {
12148
12523
  } catch {
12149
12524
  }
12150
12525
  try {
12151
- fs42.unlinkSync(lockPath);
12526
+ fs43.unlinkSync(lockPath);
12152
12527
  } catch {
12153
12528
  }
12154
12529
  }
12155
12530
  try {
12156
- fs42.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
12531
+ fs43.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
12157
12532
  return true;
12158
12533
  } catch {
12159
12534
  return false;
@@ -12170,30 +12545,30 @@ function isProcessAlive(pid) {
12170
12545
  function releaseFinalizeLock() {
12171
12546
  const lockPath = lockFilePath();
12172
12547
  try {
12173
- if (fs42.existsSync(lockPath)) fs42.unlinkSync(lockPath);
12548
+ if (fs43.existsSync(lockPath)) fs43.unlinkSync(lockPath);
12174
12549
  } catch {
12175
12550
  }
12176
12551
  }
12177
12552
 
12178
12553
  // src/lib/notifications.ts
12179
- import fs43 from "fs";
12180
- import path35 from "path";
12554
+ import fs44 from "fs";
12555
+ import path36 from "path";
12181
12556
  import chalk22 from "chalk";
12182
12557
  function notificationFilePath() {
12183
- return path35.join(getLearningDir(), "last-finalize-summary.json");
12558
+ return path36.join(getLearningDir(), "last-finalize-summary.json");
12184
12559
  }
12185
12560
  function writeFinalizeSummary(summary) {
12186
12561
  try {
12187
12562
  ensureLearningDir();
12188
- fs43.writeFileSync(notificationFilePath(), JSON.stringify(summary, null, 2));
12563
+ fs44.writeFileSync(notificationFilePath(), JSON.stringify(summary, null, 2));
12189
12564
  } catch {
12190
12565
  }
12191
12566
  }
12192
12567
  function checkPendingNotifications() {
12193
12568
  try {
12194
- if (!fs43.existsSync(notificationFilePath())) return;
12195
- const raw = fs43.readFileSync(notificationFilePath(), "utf-8");
12196
- fs43.unlinkSync(notificationFilePath());
12569
+ if (!fs44.existsSync(notificationFilePath())) return;
12570
+ const raw = fs44.readFileSync(notificationFilePath(), "utf-8");
12571
+ fs44.unlinkSync(notificationFilePath());
12197
12572
  const summary = JSON.parse(raw);
12198
12573
  if (!summary.newItemCount || summary.newItemCount === 0) return;
12199
12574
  const wasteLabel = summary.wasteTokens > 0 ? ` (~${summary.wasteTokens.toLocaleString()} wasted tokens captured)` : "";
@@ -12209,7 +12584,7 @@ function checkPendingNotifications() {
12209
12584
  console.log("");
12210
12585
  } catch {
12211
12586
  try {
12212
- fs43.unlinkSync(notificationFilePath());
12587
+ fs44.unlinkSync(notificationFilePath());
12213
12588
  } catch {
12214
12589
  }
12215
12590
  }
@@ -12361,8 +12736,8 @@ function calculateSessionWaste(events) {
12361
12736
  init_config();
12362
12737
 
12363
12738
  // src/learner/roi.ts
12364
- import fs44 from "fs";
12365
- import path36 from "path";
12739
+ import fs45 from "fs";
12740
+ import path37 from "path";
12366
12741
  var DEFAULT_TOTALS = {
12367
12742
  totalWasteTokens: 0,
12368
12743
  totalWasteSeconds: 0,
@@ -12376,19 +12751,19 @@ var DEFAULT_TOTALS = {
12376
12751
  lastSessionTimestamp: ""
12377
12752
  };
12378
12753
  function roiFilePath() {
12379
- return path36.join(getLearningDir(), LEARNING_ROI_FILE);
12754
+ return path37.join(getLearningDir(), LEARNING_ROI_FILE);
12380
12755
  }
12381
12756
  function readROIStats() {
12382
12757
  const filePath = roiFilePath();
12383
- if (!fs44.existsSync(filePath)) {
12758
+ if (!fs45.existsSync(filePath)) {
12384
12759
  return { learnings: [], sessions: [], totals: { ...DEFAULT_TOTALS } };
12385
12760
  }
12386
12761
  try {
12387
- return JSON.parse(fs44.readFileSync(filePath, "utf-8"));
12762
+ return JSON.parse(fs45.readFileSync(filePath, "utf-8"));
12388
12763
  } catch {
12389
12764
  try {
12390
12765
  const corruptPath = filePath + ".corrupt";
12391
- fs44.renameSync(filePath, corruptPath);
12766
+ fs45.renameSync(filePath, corruptPath);
12392
12767
  console.error(`caliber: roi-stats.json was corrupt \u2014 renamed to ${corruptPath}`);
12393
12768
  } catch {
12394
12769
  }
@@ -12397,7 +12772,7 @@ function readROIStats() {
12397
12772
  }
12398
12773
  function writeROIStats(stats) {
12399
12774
  ensureLearningDir();
12400
- fs44.writeFileSync(roiFilePath(), JSON.stringify(stats, null, 2));
12775
+ fs45.writeFileSync(roiFilePath(), JSON.stringify(stats, null, 2));
12401
12776
  }
12402
12777
  function recalculateTotals(stats) {
12403
12778
  const totals = stats.totals;
@@ -12606,9 +12981,9 @@ var AUTO_SETTLE_MS = 200;
12606
12981
  var INCREMENTAL_INTERVAL = 50;
12607
12982
  function writeFinalizeError(message) {
12608
12983
  try {
12609
- const errorPath = path37.join(getLearningDir(), LEARNING_LAST_ERROR_FILE);
12610
- if (!fs45.existsSync(getLearningDir())) fs45.mkdirSync(getLearningDir(), { recursive: true });
12611
- fs45.writeFileSync(errorPath, JSON.stringify({
12984
+ const errorPath = path38.join(getLearningDir(), LEARNING_LAST_ERROR_FILE);
12985
+ if (!fs46.existsSync(getLearningDir())) fs46.mkdirSync(getLearningDir(), { recursive: true });
12986
+ fs46.writeFileSync(errorPath, JSON.stringify({
12612
12987
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12613
12988
  error: message,
12614
12989
  pid: process.pid
@@ -12618,9 +12993,9 @@ function writeFinalizeError(message) {
12618
12993
  }
12619
12994
  function readFinalizeError() {
12620
12995
  try {
12621
- const errorPath = path37.join(getLearningDir(), LEARNING_LAST_ERROR_FILE);
12622
- if (!fs45.existsSync(errorPath)) return null;
12623
- return JSON.parse(fs45.readFileSync(errorPath, "utf-8"));
12996
+ const errorPath = path38.join(getLearningDir(), LEARNING_LAST_ERROR_FILE);
12997
+ if (!fs46.existsSync(errorPath)) return null;
12998
+ return JSON.parse(fs46.readFileSync(errorPath, "utf-8"));
12624
12999
  } catch {
12625
13000
  return null;
12626
13001
  }
@@ -12667,14 +13042,14 @@ async function learnObserveCommand(options) {
12667
13042
  const { resolveCaliber: resolveCaliber2 } = await Promise.resolve().then(() => (init_resolve_caliber(), resolve_caliber_exports));
12668
13043
  const bin = resolveCaliber2();
12669
13044
  const { spawn: spawn4 } = await import("child_process");
12670
- const logPath = path37.join(getLearningDir(), LEARNING_FINALIZE_LOG);
12671
- if (!fs45.existsSync(getLearningDir())) fs45.mkdirSync(getLearningDir(), { recursive: true });
12672
- const logFd = fs45.openSync(logPath, "a");
13045
+ const logPath = path38.join(getLearningDir(), LEARNING_FINALIZE_LOG);
13046
+ if (!fs46.existsSync(getLearningDir())) fs46.mkdirSync(getLearningDir(), { recursive: true });
13047
+ const logFd = fs46.openSync(logPath, "a");
12673
13048
  spawn4(bin, ["learn", "finalize", "--auto", "--incremental"], {
12674
13049
  detached: true,
12675
13050
  stdio: ["ignore", logFd, logFd]
12676
13051
  }).unref();
12677
- fs45.closeSync(logFd);
13052
+ fs46.closeSync(logFd);
12678
13053
  } catch {
12679
13054
  }
12680
13055
  }
@@ -12881,7 +13256,7 @@ async function learnFinalizeCommand(options) {
12881
13256
  }
12882
13257
  async function learnInstallCommand() {
12883
13258
  let anyInstalled = false;
12884
- if (fs45.existsSync(".claude")) {
13259
+ if (fs46.existsSync(".claude")) {
12885
13260
  const r = installLearningHooks();
12886
13261
  if (r.installed) {
12887
13262
  console.log(chalk23.green("\u2713") + " Claude Code learning hooks installed");
@@ -12890,7 +13265,7 @@ async function learnInstallCommand() {
12890
13265
  console.log(chalk23.dim(" Claude Code hooks already installed"));
12891
13266
  }
12892
13267
  }
12893
- if (fs45.existsSync(".cursor")) {
13268
+ if (fs46.existsSync(".cursor")) {
12894
13269
  const r = installCursorLearningHooks();
12895
13270
  if (r.installed) {
12896
13271
  console.log(chalk23.green("\u2713") + " Cursor learning hooks installed");
@@ -12899,7 +13274,7 @@ async function learnInstallCommand() {
12899
13274
  console.log(chalk23.dim(" Cursor hooks already installed"));
12900
13275
  }
12901
13276
  }
12902
- if (!fs45.existsSync(".claude") && !fs45.existsSync(".cursor")) {
13277
+ if (!fs46.existsSync(".claude") && !fs46.existsSync(".cursor")) {
12903
13278
  console.log(chalk23.yellow("No .claude/ or .cursor/ directory found."));
12904
13279
  console.log(chalk23.dim(` Run \`${resolveCaliber()} init\` first, or create the directory manually.`));
12905
13280
  return;
@@ -12957,8 +13332,8 @@ async function learnStatusCommand() {
12957
13332
  if (lastError) {
12958
13333
  console.log(`Last error: ${chalk23.red(lastError.error)}`);
12959
13334
  console.log(chalk23.dim(` at ${lastError.timestamp}`));
12960
- const logPath = path37.join(getLearningDir(), LEARNING_FINALIZE_LOG);
12961
- if (fs45.existsSync(logPath)) {
13335
+ const logPath = path38.join(getLearningDir(), LEARNING_FINALIZE_LOG);
13336
+ if (fs46.existsSync(logPath)) {
12962
13337
  console.log(chalk23.dim(` Full log: ${logPath}`));
12963
13338
  }
12964
13339
  }
@@ -13038,11 +13413,11 @@ async function learnDeleteCommand(indexStr) {
13038
13413
  }
13039
13414
  const item = items[targetIdx];
13040
13415
  const filePath = item.source === "personal" ? PERSONAL_LEARNINGS_FILE : "CALIBER_LEARNINGS.md";
13041
- if (!fs45.existsSync(filePath)) {
13416
+ if (!fs46.existsSync(filePath)) {
13042
13417
  console.log(chalk23.red("Learnings file not found."));
13043
13418
  return;
13044
13419
  }
13045
- const content = fs45.readFileSync(filePath, "utf-8");
13420
+ const content = fs46.readFileSync(filePath, "utf-8");
13046
13421
  const lines = content.split("\n");
13047
13422
  const bulletsOfSource = items.filter((i) => i.source === item.source);
13048
13423
  const posInFile = bulletsOfSource.indexOf(item);
@@ -13063,9 +13438,9 @@ async function learnDeleteCommand(indexStr) {
13063
13438
  }
13064
13439
  const bulletToRemove = lines[lineToRemove];
13065
13440
  const newLines = lines.filter((_, i) => i !== lineToRemove);
13066
- fs45.writeFileSync(filePath, newLines.join("\n"));
13441
+ fs46.writeFileSync(filePath, newLines.join("\n"));
13067
13442
  if (item.source === "personal") {
13068
- fs45.chmodSync(filePath, 384);
13443
+ fs46.chmodSync(filePath, 384);
13069
13444
  }
13070
13445
  const roiStats = readROIStats();
13071
13446
  const cleanText = bulletToRemove.replace(/^- /, "").replace(/^\*\*\[[^\]]+\]\*\*\s*/, "").trim();
@@ -13238,8 +13613,8 @@ async function insightsCommand(options) {
13238
13613
  }
13239
13614
 
13240
13615
  // src/commands/sources.ts
13241
- import fs46 from "fs";
13242
- import path38 from "path";
13616
+ import fs47 from "fs";
13617
+ import path39 from "path";
13243
13618
  import chalk25 from "chalk";
13244
13619
  init_resolve_caliber();
13245
13620
  async function sourcesListCommand() {
@@ -13256,9 +13631,9 @@ async function sourcesListCommand() {
13256
13631
  if (configSources.length > 0) {
13257
13632
  for (const source of configSources) {
13258
13633
  const sourcePath = source.path || source.url || "";
13259
- const exists = source.path ? fs46.existsSync(path38.resolve(dir, source.path)) : false;
13634
+ const exists = source.path ? fs47.existsSync(path39.resolve(dir, source.path)) : false;
13260
13635
  const status = exists ? chalk25.green("reachable") : chalk25.red("not found");
13261
- const hasSummary = source.path && fs46.existsSync(path38.join(path38.resolve(dir, source.path), ".caliber", "summary.json"));
13636
+ const hasSummary = source.path && fs47.existsSync(path39.join(path39.resolve(dir, source.path), ".caliber", "summary.json"));
13262
13637
  console.log(` ${chalk25.bold(source.role || source.type)} ${chalk25.dim(sourcePath)}`);
13263
13638
  console.log(` Type: ${source.type} Status: ${status}${hasSummary ? " " + chalk25.cyan("has summary.json") : ""}`);
13264
13639
  if (source.description) console.log(` ${chalk25.dim(source.description)}`);
@@ -13268,7 +13643,7 @@ async function sourcesListCommand() {
13268
13643
  if (workspaces.length > 0) {
13269
13644
  console.log(chalk25.dim(" Auto-detected workspaces:"));
13270
13645
  for (const ws of workspaces) {
13271
- const exists = fs46.existsSync(path38.resolve(dir, ws));
13646
+ const exists = fs47.existsSync(path39.resolve(dir, ws));
13272
13647
  console.log(` ${exists ? chalk25.green("\u25CF") : chalk25.red("\u25CF")} ${ws}`);
13273
13648
  }
13274
13649
  console.log("");
@@ -13276,8 +13651,8 @@ async function sourcesListCommand() {
13276
13651
  }
13277
13652
  async function sourcesAddCommand(sourcePath) {
13278
13653
  const dir = process.cwd();
13279
- const absPath = path38.resolve(dir, sourcePath);
13280
- if (!fs46.existsSync(absPath)) {
13654
+ const absPath = path39.resolve(dir, sourcePath);
13655
+ if (!fs47.existsSync(absPath)) {
13281
13656
  console.log(chalk25.red(`
13282
13657
  Path not found: ${sourcePath}
13283
13658
  `));
@@ -13292,7 +13667,7 @@ async function sourcesAddCommand(sourcePath) {
13292
13667
  }
13293
13668
  const existing = loadSourcesConfig(dir);
13294
13669
  const alreadyConfigured = existing.some(
13295
- (s) => s.path && path38.resolve(dir, s.path) === absPath
13670
+ (s) => s.path && path39.resolve(dir, s.path) === absPath
13296
13671
  );
13297
13672
  if (alreadyConfigured) {
13298
13673
  console.log(chalk25.yellow(`
@@ -13340,8 +13715,8 @@ async function sourcesRemoveCommand(name) {
13340
13715
  }
13341
13716
 
13342
13717
  // src/commands/publish.ts
13343
- import fs47 from "fs";
13344
- import path39 from "path";
13718
+ import fs48 from "fs";
13719
+ import path40 from "path";
13345
13720
  import chalk26 from "chalk";
13346
13721
  import ora7 from "ora";
13347
13722
  init_config();
@@ -13356,10 +13731,10 @@ async function publishCommand() {
13356
13731
  const spinner = ora7("Generating project summary...").start();
13357
13732
  try {
13358
13733
  const fingerprint = await collectFingerprint(dir);
13359
- const claudeMd = readFileOrNull(path39.join(dir, "CLAUDE.md"));
13734
+ const claudeMd = readFileOrNull(path40.join(dir, "CLAUDE.md"));
13360
13735
  const topLevelDirs = fingerprint.fileTree.filter((f) => f.endsWith("/") && !f.includes("/")).map((f) => f.replace(/\/$/, ""));
13361
13736
  const summary = {
13362
- name: fingerprint.packageName || path39.basename(dir),
13737
+ name: fingerprint.packageName || path40.basename(dir),
13363
13738
  version: "1.0.0",
13364
13739
  description: fingerprint.description || "",
13365
13740
  languages: fingerprint.languages,
@@ -13371,7 +13746,7 @@ async function publishCommand() {
13371
13746
  summary.conventions = claudeMd.slice(0, 2e3);
13372
13747
  }
13373
13748
  try {
13374
- const pkgContent = readFileOrNull(path39.join(dir, "package.json"));
13749
+ const pkgContent = readFileOrNull(path40.join(dir, "package.json"));
13375
13750
  if (pkgContent) {
13376
13751
  const pkg3 = JSON.parse(pkgContent);
13377
13752
  if (pkg3.scripts) {
@@ -13384,14 +13759,14 @@ async function publishCommand() {
13384
13759
  }
13385
13760
  } catch {
13386
13761
  }
13387
- const outputDir = path39.join(dir, ".caliber");
13388
- if (!fs47.existsSync(outputDir)) {
13389
- fs47.mkdirSync(outputDir, { recursive: true });
13762
+ const outputDir = path40.join(dir, ".caliber");
13763
+ if (!fs48.existsSync(outputDir)) {
13764
+ fs48.mkdirSync(outputDir, { recursive: true });
13390
13765
  }
13391
- const outputPath = path39.join(outputDir, "summary.json");
13392
- fs47.writeFileSync(outputPath, JSON.stringify(summary, null, 2) + "\n", "utf-8");
13766
+ const outputPath = path40.join(outputDir, "summary.json");
13767
+ fs48.writeFileSync(outputPath, JSON.stringify(summary, null, 2) + "\n", "utf-8");
13393
13768
  spinner.succeed("Project summary published");
13394
- console.log(` ${chalk26.green("\u2713")} ${path39.relative(dir, outputPath)}`);
13769
+ console.log(` ${chalk26.green("\u2713")} ${path40.relative(dir, outputPath)}`);
13395
13770
  console.log(chalk26.dim("\n Other projects can now reference this repo as a source."));
13396
13771
  console.log(chalk26.dim(" When they run `caliber init`, they'll read this summary automatically.\n"));
13397
13772
  } catch (err) {
@@ -13404,7 +13779,7 @@ async function publishCommand() {
13404
13779
 
13405
13780
  // src/commands/bootstrap.ts
13406
13781
  init_builtin_skills();
13407
- import fs48 from "fs";
13782
+ import fs49 from "fs";
13408
13783
  import chalk27 from "chalk";
13409
13784
  var PLATFORM_SKILL_DIRS = {
13410
13785
  claude: ".claude/skills",
@@ -13425,8 +13800,8 @@ async function bootstrapCommand() {
13425
13800
  for (const skill of BUILTIN_SKILLS) {
13426
13801
  const skillDir = `${skillsDir}/${skill.name}`;
13427
13802
  const skillPath = `${skillDir}/SKILL.md`;
13428
- fs48.mkdirSync(skillDir, { recursive: true });
13429
- fs48.writeFileSync(skillPath, buildSkillContent(skill));
13803
+ fs49.mkdirSync(skillDir, { recursive: true });
13804
+ fs49.writeFileSync(skillPath, buildSkillContent(skill));
13430
13805
  written.push(skillPath);
13431
13806
  }
13432
13807
  }
@@ -13443,8 +13818,8 @@ async function bootstrapCommand() {
13443
13818
  }
13444
13819
 
13445
13820
  // src/commands/uninstall.ts
13446
- import fs49 from "fs";
13447
- import path40 from "path";
13821
+ import fs50 from "fs";
13822
+ import path41 from "path";
13448
13823
  import chalk28 from "chalk";
13449
13824
  import confirm3 from "@inquirer/confirm";
13450
13825
  init_pre_commit_block();
@@ -13452,18 +13827,19 @@ init_builtin_skills();
13452
13827
  init_config();
13453
13828
  var MANAGED_DOC_FILES = [
13454
13829
  "CLAUDE.md",
13455
- path40.join(".github", "copilot-instructions.md"),
13830
+ path41.join(".github", "copilot-instructions.md"),
13456
13831
  "AGENTS.md"
13457
13832
  ];
13458
13833
  var SKILL_DIRS = PLATFORM_CONFIGS.map((c) => c.skillsDir);
13459
- var CURSOR_RULES_DIR = path40.join(".cursor", "rules");
13460
- function removeCaliberCursorRules() {
13834
+ var CURSOR_RULES_DIR = path41.join(".cursor", "rules");
13835
+ var CLAUDE_RULES_DIR = path41.join(".claude", "rules");
13836
+ function removeCaliberManagedFiles(dir, extension) {
13461
13837
  const removed = [];
13462
- if (!fs49.existsSync(CURSOR_RULES_DIR)) return removed;
13463
- for (const file of fs49.readdirSync(CURSOR_RULES_DIR)) {
13464
- if (file.startsWith("caliber-") && file.endsWith(".mdc")) {
13465
- const fullPath = path40.join(CURSOR_RULES_DIR, file);
13466
- fs49.unlinkSync(fullPath);
13838
+ if (!fs50.existsSync(dir)) return removed;
13839
+ for (const file of fs50.readdirSync(dir)) {
13840
+ if (file.startsWith(CALIBER_MANAGED_PREFIX) && file.endsWith(extension)) {
13841
+ const fullPath = path41.join(dir, file);
13842
+ fs50.unlinkSync(fullPath);
13467
13843
  removed.push(fullPath);
13468
13844
  }
13469
13845
  }
@@ -13472,11 +13848,11 @@ function removeCaliberCursorRules() {
13472
13848
  function removeBuiltinSkills() {
13473
13849
  const removed = [];
13474
13850
  for (const skillsDir of SKILL_DIRS) {
13475
- if (!fs49.existsSync(skillsDir)) continue;
13851
+ if (!fs50.existsSync(skillsDir)) continue;
13476
13852
  for (const name of BUILTIN_SKILL_NAMES) {
13477
- const skillDir = path40.join(skillsDir, name);
13478
- if (fs49.existsSync(skillDir)) {
13479
- fs49.rmSync(skillDir, { recursive: true });
13853
+ const skillDir = path41.join(skillsDir, name);
13854
+ if (fs50.existsSync(skillDir)) {
13855
+ fs50.rmSync(skillDir, { recursive: true });
13480
13856
  removed.push(skillDir);
13481
13857
  }
13482
13858
  }
@@ -13486,15 +13862,15 @@ function removeBuiltinSkills() {
13486
13862
  function stripManagedBlocksFromFiles() {
13487
13863
  const modified = [];
13488
13864
  for (const filePath of MANAGED_DOC_FILES) {
13489
- if (!fs49.existsSync(filePath)) continue;
13490
- const original = fs49.readFileSync(filePath, "utf-8");
13865
+ if (!fs50.existsSync(filePath)) continue;
13866
+ const original = fs50.readFileSync(filePath, "utf-8");
13491
13867
  const stripped = stripManagedBlocks(original);
13492
13868
  if (stripped !== original) {
13493
13869
  const trimmed = stripped.trim();
13494
13870
  if (!trimmed || /^#\s*\S*$/.test(trimmed)) {
13495
- fs49.unlinkSync(filePath);
13871
+ fs50.unlinkSync(filePath);
13496
13872
  } else {
13497
- fs49.writeFileSync(filePath, stripped);
13873
+ fs50.writeFileSync(filePath, stripped);
13498
13874
  }
13499
13875
  modified.push(filePath);
13500
13876
  }
@@ -13502,8 +13878,8 @@ function stripManagedBlocksFromFiles() {
13502
13878
  return modified;
13503
13879
  }
13504
13880
  function removeDirectory(dir) {
13505
- if (!fs49.existsSync(dir)) return false;
13506
- fs49.rmSync(dir, { recursive: true });
13881
+ if (!fs50.existsSync(dir)) return false;
13882
+ fs50.rmSync(dir, { recursive: true });
13507
13883
  return true;
13508
13884
  }
13509
13885
  async function uninstallCommand(options) {
@@ -13535,6 +13911,16 @@ async function uninstallCommand(options) {
13535
13911
  console.log(` ${chalk28.red("\u2717")} Onboarding hook removed`);
13536
13912
  actions.push("onboarding hook");
13537
13913
  }
13914
+ const notificationHookResult = removeNotificationHook();
13915
+ if (notificationHookResult.removed) {
13916
+ console.log(` ${chalk28.red("\u2717")} Notification hook removed`);
13917
+ actions.push("notification hook");
13918
+ }
13919
+ const sessionStartResult = removeSessionStartHook();
13920
+ if (sessionStartResult.removed) {
13921
+ console.log(` ${chalk28.red("\u2717")} SessionStart hook removed`);
13922
+ actions.push("session-start hook");
13923
+ }
13538
13924
  const learnResult = removeLearningHooks();
13539
13925
  if (learnResult.removed) {
13540
13926
  console.log(` ${chalk28.red("\u2717")} Claude Code learning hooks removed`);
@@ -13550,18 +13936,23 @@ async function uninstallCommand(options) {
13550
13936
  console.log(` ${chalk28.yellow("~")} ${file} \u2014 managed blocks removed`);
13551
13937
  actions.push(file);
13552
13938
  }
13553
- const removedRules = removeCaliberCursorRules();
13554
- for (const rule of removedRules) {
13939
+ const removedCursorRules = removeCaliberManagedFiles(CURSOR_RULES_DIR, ".mdc");
13940
+ for (const rule of removedCursorRules) {
13555
13941
  console.log(` ${chalk28.red("\u2717")} ${rule}`);
13556
13942
  }
13557
- if (removedRules.length > 0) actions.push("cursor rules");
13943
+ if (removedCursorRules.length > 0) actions.push("cursor rules");
13944
+ const removedClaudeRules = removeCaliberManagedFiles(CLAUDE_RULES_DIR, ".md");
13945
+ for (const rule of removedClaudeRules) {
13946
+ console.log(` ${chalk28.red("\u2717")} ${rule}`);
13947
+ }
13948
+ if (removedClaudeRules.length > 0) actions.push("claude rules");
13558
13949
  const removedSkills = removeBuiltinSkills();
13559
13950
  for (const skill of removedSkills) {
13560
13951
  console.log(` ${chalk28.red("\u2717")} ${skill}/`);
13561
13952
  }
13562
13953
  if (removedSkills.length > 0) actions.push("builtin skills");
13563
- if (fs49.existsSync("CALIBER_LEARNINGS.md")) {
13564
- fs49.unlinkSync("CALIBER_LEARNINGS.md");
13954
+ if (fs50.existsSync("CALIBER_LEARNINGS.md")) {
13955
+ fs50.unlinkSync("CALIBER_LEARNINGS.md");
13565
13956
  console.log(` ${chalk28.red("\u2717")} CALIBER_LEARNINGS.md`);
13566
13957
  actions.push("learnings file");
13567
13958
  }
@@ -13575,18 +13966,18 @@ async function uninstallCommand(options) {
13575
13966
  }
13576
13967
  trackUninstallExecuted();
13577
13968
  const configPath = getConfigFilePath();
13578
- if (fs49.existsSync(configPath)) {
13969
+ if (fs50.existsSync(configPath)) {
13579
13970
  console.log("");
13580
13971
  const removeConfig = options.force || await confirm3({
13581
13972
  message: `Remove global config (~/.caliber/config.json)? This affects all projects.`
13582
13973
  });
13583
13974
  if (removeConfig) {
13584
- fs49.unlinkSync(configPath);
13975
+ fs50.unlinkSync(configPath);
13585
13976
  console.log(` ${chalk28.red("\u2717")} ${configPath}`);
13586
- const configDir = path40.dirname(configPath);
13977
+ const configDir = path41.dirname(configPath);
13587
13978
  try {
13588
- const remaining = fs49.readdirSync(configDir);
13589
- if (remaining.length === 0) fs49.rmdirSync(configDir);
13979
+ const remaining = fs50.readdirSync(configDir);
13980
+ if (remaining.length === 0) fs50.rmdirSync(configDir);
13590
13981
  } catch {
13591
13982
  }
13592
13983
  }
@@ -13597,8 +13988,8 @@ async function uninstallCommand(options) {
13597
13988
  }
13598
13989
 
13599
13990
  // src/cli.ts
13600
- var __dirname = path41.dirname(fileURLToPath(import.meta.url));
13601
- var pkg = JSON.parse(fs50.readFileSync(path41.resolve(__dirname, "..", "package.json"), "utf-8"));
13991
+ var __dirname = path42.dirname(fileURLToPath(import.meta.url));
13992
+ var pkg = JSON.parse(fs51.readFileSync(path42.resolve(__dirname, "..", "package.json"), "utf-8"));
13602
13993
  var program = new Command();
13603
13994
  var displayVersion = process.env.CALIBER_LOCAL ? `${pkg.version}-local` : pkg.version;
13604
13995
  program.name(process.env.CALIBER_LOCAL ? "caloc" : "caliber").description("AI context infrastructure for coding agents").version(displayVersion).option("--no-traces", "Disable anonymous telemetry for this run");
@@ -13709,15 +14100,15 @@ learn.command("delete <index>").description("Delete a learning by its index numb
13709
14100
  learn.command("add <content>").description("Add a learning directly (used by agent skills)").option("--personal", "Save as a personal learning instead of project-level").action(tracked("learn:add", learnAddCommand));
13710
14101
 
13711
14102
  // src/utils/version-check.ts
13712
- import fs51 from "fs";
13713
- import path42 from "path";
14103
+ import fs52 from "fs";
14104
+ import path43 from "path";
13714
14105
  import { fileURLToPath as fileURLToPath2 } from "url";
13715
14106
  import { execSync as execSync16, execFileSync as execFileSync3 } from "child_process";
13716
14107
  import chalk29 from "chalk";
13717
14108
  import ora8 from "ora";
13718
14109
  import confirm4 from "@inquirer/confirm";
13719
- var __dirname_vc = path42.dirname(fileURLToPath2(import.meta.url));
13720
- var pkg2 = JSON.parse(fs51.readFileSync(path42.resolve(__dirname_vc, "..", "package.json"), "utf-8"));
14110
+ var __dirname_vc = path43.dirname(fileURLToPath2(import.meta.url));
14111
+ var pkg2 = JSON.parse(fs52.readFileSync(path43.resolve(__dirname_vc, "..", "package.json"), "utf-8"));
13721
14112
  function getChannel(version) {
13722
14113
  const match = version.match(/-(dev|next)\./);
13723
14114
  return match ? match[1] : "latest";
@@ -13744,8 +14135,8 @@ function getInstalledVersion() {
13744
14135
  encoding: "utf-8",
13745
14136
  stdio: ["pipe", "pipe", "pipe"]
13746
14137
  }).trim();
13747
- const pkgPath = path42.join(globalRoot, "@rely-ai", "caliber", "package.json");
13748
- return JSON.parse(fs51.readFileSync(pkgPath, "utf-8")).version;
14138
+ const pkgPath = path43.join(globalRoot, "@rely-ai", "caliber", "package.json");
14139
+ return JSON.parse(fs52.readFileSync(pkgPath, "utf-8")).version;
13749
14140
  } catch {
13750
14141
  return null;
13751
14142
  }