@rely-ai/caliber 1.31.0-dev.1774714955 → 1.31.0-dev.1774743306

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 +779 -587
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -9,155 +9,6 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/llm/config.ts
13
- var config_exports = {};
14
- __export(config_exports, {
15
- DEFAULT_FAST_MODELS: () => DEFAULT_FAST_MODELS,
16
- DEFAULT_MODELS: () => DEFAULT_MODELS,
17
- MODEL_CONTEXT_WINDOWS: () => MODEL_CONTEXT_WINDOWS,
18
- getConfigFilePath: () => getConfigFilePath,
19
- getDisplayModel: () => getDisplayModel,
20
- getFastModel: () => getFastModel,
21
- getMaxPromptTokens: () => getMaxPromptTokens,
22
- loadConfig: () => loadConfig,
23
- readConfigFile: () => readConfigFile,
24
- resolveFromEnv: () => resolveFromEnv,
25
- writeConfigFile: () => writeConfigFile
26
- });
27
- import fs4 from "fs";
28
- import path5 from "path";
29
- import os2 from "os";
30
- function getMaxPromptTokens() {
31
- const config = loadConfig();
32
- const model = process.env.CALIBER_MODEL || config?.model;
33
- const contextWindow = model ? MODEL_CONTEXT_WINDOWS[model] ?? DEFAULT_CONTEXT_WINDOW : DEFAULT_CONTEXT_WINDOW;
34
- const budget = Math.floor(contextWindow * INPUT_BUDGET_FRACTION);
35
- return Math.max(MIN_PROMPT_TOKENS, Math.min(budget, MAX_PROMPT_TOKENS_CAP));
36
- }
37
- function loadConfig() {
38
- const envConfig = resolveFromEnv();
39
- if (envConfig) return envConfig;
40
- return readConfigFile();
41
- }
42
- function resolveFromEnv() {
43
- if (process.env.ANTHROPIC_API_KEY) {
44
- return {
45
- provider: "anthropic",
46
- apiKey: process.env.ANTHROPIC_API_KEY,
47
- model: process.env.CALIBER_MODEL || DEFAULT_MODELS.anthropic
48
- };
49
- }
50
- if (process.env.VERTEX_PROJECT_ID || process.env.GCP_PROJECT_ID) {
51
- return {
52
- provider: "vertex",
53
- model: process.env.CALIBER_MODEL || DEFAULT_MODELS.vertex,
54
- vertexProjectId: process.env.VERTEX_PROJECT_ID || process.env.GCP_PROJECT_ID,
55
- vertexRegion: process.env.VERTEX_REGION || process.env.GCP_REGION || "us-east5",
56
- vertexCredentials: process.env.VERTEX_SA_CREDENTIALS || process.env.GOOGLE_APPLICATION_CREDENTIALS
57
- };
58
- }
59
- if (process.env.OPENAI_API_KEY) {
60
- return {
61
- provider: "openai",
62
- apiKey: process.env.OPENAI_API_KEY,
63
- model: process.env.CALIBER_MODEL || DEFAULT_MODELS.openai,
64
- baseUrl: process.env.OPENAI_BASE_URL
65
- };
66
- }
67
- if (process.env.CALIBER_USE_CURSOR_SEAT === "1" || process.env.CALIBER_USE_CURSOR_SEAT === "true") {
68
- return {
69
- provider: "cursor",
70
- model: process.env.CALIBER_MODEL || DEFAULT_MODELS.cursor
71
- };
72
- }
73
- if (process.env.CALIBER_USE_CLAUDE_CLI === "1" || process.env.CALIBER_USE_CLAUDE_CLI === "true") {
74
- return {
75
- provider: "claude-cli",
76
- model: process.env.CALIBER_MODEL || DEFAULT_MODELS["claude-cli"]
77
- };
78
- }
79
- return null;
80
- }
81
- function readConfigFile() {
82
- try {
83
- if (!fs4.existsSync(CONFIG_FILE)) return null;
84
- const raw = fs4.readFileSync(CONFIG_FILE, "utf-8");
85
- const parsed = JSON.parse(raw);
86
- if (!parsed.provider || !["anthropic", "vertex", "openai", "cursor", "claude-cli"].includes(parsed.provider)) {
87
- return null;
88
- }
89
- return parsed;
90
- } catch {
91
- return null;
92
- }
93
- }
94
- function writeConfigFile(config) {
95
- if (!fs4.existsSync(CONFIG_DIR)) {
96
- fs4.mkdirSync(CONFIG_DIR, { recursive: true });
97
- }
98
- const sanitized = { ...config };
99
- if (sanitized.apiKey) {
100
- sanitized.apiKey = sanitized.apiKey.trim();
101
- }
102
- fs4.writeFileSync(CONFIG_FILE, JSON.stringify(sanitized, null, 2) + "\n", { mode: 384 });
103
- }
104
- function getConfigFilePath() {
105
- return CONFIG_FILE;
106
- }
107
- function getDisplayModel(config) {
108
- if (config.model === "default" && config.provider === "claude-cli") {
109
- return process.env.ANTHROPIC_MODEL || "default (inherited from Claude Code)";
110
- }
111
- return config.model;
112
- }
113
- function getFastModel() {
114
- if (process.env.CALIBER_FAST_MODEL) return process.env.CALIBER_FAST_MODEL;
115
- const config = loadConfig();
116
- const provider = config?.provider;
117
- if (process.env.ANTHROPIC_SMALL_FAST_MODEL && (!provider || provider === "anthropic" || provider === "vertex" || provider === "claude-cli")) {
118
- return process.env.ANTHROPIC_SMALL_FAST_MODEL;
119
- }
120
- if (config?.fastModel) return config.fastModel;
121
- if (provider) return DEFAULT_FAST_MODELS[provider];
122
- return void 0;
123
- }
124
- var CONFIG_DIR, CONFIG_FILE, DEFAULT_MODELS, MODEL_CONTEXT_WINDOWS, DEFAULT_CONTEXT_WINDOW, INPUT_BUDGET_FRACTION, MAX_PROMPT_TOKENS_CAP, MIN_PROMPT_TOKENS, DEFAULT_FAST_MODELS;
125
- var init_config = __esm({
126
- "src/llm/config.ts"() {
127
- "use strict";
128
- CONFIG_DIR = path5.join(os2.homedir(), ".caliber");
129
- CONFIG_FILE = path5.join(CONFIG_DIR, "config.json");
130
- DEFAULT_MODELS = {
131
- anthropic: "claude-sonnet-4-6",
132
- vertex: "claude-sonnet-4-6",
133
- openai: "gpt-4.1",
134
- cursor: "sonnet-4.6",
135
- "claude-cli": "default"
136
- };
137
- MODEL_CONTEXT_WINDOWS = {
138
- "claude-sonnet-4-6": 2e5,
139
- "claude-opus-4-6": 2e5,
140
- "claude-haiku-4-5-20251001": 2e5,
141
- "claude-sonnet-4-5-20250514": 2e5,
142
- "gpt-4.1": 1e6,
143
- "gpt-4.1-mini": 1e6,
144
- "gpt-4o": 128e3,
145
- "gpt-4o-mini": 128e3,
146
- "sonnet-4.6": 2e5
147
- };
148
- DEFAULT_CONTEXT_WINDOW = 2e5;
149
- INPUT_BUDGET_FRACTION = 0.6;
150
- MAX_PROMPT_TOKENS_CAP = 3e5;
151
- MIN_PROMPT_TOKENS = 3e4;
152
- DEFAULT_FAST_MODELS = {
153
- anthropic: "claude-haiku-4-5-20251001",
154
- vertex: "claude-haiku-4-5-20251001",
155
- openai: "gpt-4.1-mini",
156
- cursor: "gpt-5.3-codex-fast"
157
- };
158
- }
159
- });
160
-
161
12
  // src/lib/resolve-caliber.ts
162
13
  var resolve_caliber_exports = {};
163
14
  __export(resolve_caliber_exports, {
@@ -166,8 +17,8 @@ __export(resolve_caliber_exports, {
166
17
  resetResolvedCaliber: () => resetResolvedCaliber,
167
18
  resolveCaliber: () => resolveCaliber
168
19
  });
169
- import fs6 from "fs";
170
- import { execSync as execSync4 } from "child_process";
20
+ import fs2 from "fs";
21
+ import { execSync as execSync3 } from "child_process";
171
22
  function resolveCaliber() {
172
23
  if (_resolved) return _resolved;
173
24
  const isNpx = process.argv[1]?.includes("_npx") || process.env.npm_execpath?.includes("npx");
@@ -177,7 +28,7 @@ function resolveCaliber() {
177
28
  }
178
29
  try {
179
30
  const whichCmd = process.platform === "win32" ? "where caliber" : "which caliber";
180
- execSync4(whichCmd, {
31
+ execSync3(whichCmd, {
181
32
  encoding: "utf-8",
182
33
  stdio: ["pipe", "pipe", "pipe"]
183
34
  });
@@ -186,7 +37,7 @@ function resolveCaliber() {
186
37
  } catch {
187
38
  }
188
39
  const binPath = process.argv[1];
189
- if (binPath && /caliber/.test(binPath) && fs6.existsSync(binPath)) {
40
+ if (binPath && /caliber/.test(binPath) && fs2.existsSync(binPath)) {
190
41
  _resolved = binPath;
191
42
  return _resolved;
192
43
  }
@@ -214,156 +65,6 @@ var init_resolve_caliber = __esm({
214
65
  }
215
66
  });
216
67
 
217
- // src/llm/types.ts
218
- var types_exports = {};
219
- __export(types_exports, {
220
- isSeatBased: () => isSeatBased
221
- });
222
- function isSeatBased(provider) {
223
- return SEAT_BASED_PROVIDERS.has(provider);
224
- }
225
- var SEAT_BASED_PROVIDERS;
226
- var init_types = __esm({
227
- "src/llm/types.ts"() {
228
- "use strict";
229
- SEAT_BASED_PROVIDERS = /* @__PURE__ */ new Set(["cursor", "claude-cli"]);
230
- }
231
- });
232
-
233
- // src/writers/pre-commit-block.ts
234
- var pre_commit_block_exports = {};
235
- __export(pre_commit_block_exports, {
236
- appendLearningsBlock: () => appendLearningsBlock,
237
- appendPreCommitBlock: () => appendPreCommitBlock,
238
- appendSyncBlock: () => appendSyncBlock,
239
- getCursorLearningsRule: () => getCursorLearningsRule,
240
- getCursorPreCommitRule: () => getCursorPreCommitRule,
241
- getCursorSyncRule: () => getCursorSyncRule,
242
- hasLearningsBlock: () => hasLearningsBlock,
243
- hasPreCommitBlock: () => hasPreCommitBlock,
244
- hasSyncBlock: () => hasSyncBlock
245
- });
246
- function getPreCommitBlock() {
247
- const bin = resolveCaliber();
248
- return `${BLOCK_START}
249
- ## Before Committing
250
-
251
- **IMPORTANT:** Before every git commit, you MUST ensure Caliber syncs agent configs with the latest code changes.
252
-
253
- First, check if the pre-commit hook is already installed:
254
- \`\`\`bash
255
- grep -q "caliber" .git/hooks/pre-commit 2>/dev/null && echo "hook-active" || echo "no-hook"
256
- \`\`\`
257
-
258
- - If **hook-active**: the hook handles sync automatically \u2014 just commit normally. Tell the user: "Caliber will sync your agent configs automatically via the pre-commit hook."
259
- - If **no-hook**: run Caliber manually before committing:
260
- 1. Tell the user: "Caliber: Syncing agent configs with your latest changes..."
261
- 2. Run: \`${bin} refresh && git add ${MANAGED_DOC_PATHS} 2>/dev/null\`
262
- 3. After it completes, briefly tell the user what Caliber updated. Then proceed with the commit.
263
-
264
- If \`${bin}\` is not found, tell the user: "This project uses Caliber for agent config sync. Run /setup-caliber to get set up."
265
- ${BLOCK_END}`;
266
- }
267
- function getCursorRuleContent() {
268
- const bin = resolveCaliber();
269
- return `---
270
- description: Run Caliber sync before git commits to keep agent configs up to date
271
- alwaysApply: true
272
- ---
273
- Before every git commit, check if the Caliber pre-commit hook is installed:
274
- Run: \`grep -q "caliber" .git/hooks/pre-commit 2>/dev/null && echo "hook-active" || echo "no-hook"\`
275
-
276
- If hook-active: just commit normally \u2014 Caliber syncs automatically via the hook. Tell the user: "Caliber will sync your agent configs automatically."
277
- If no-hook: run \`${bin} refresh && git add ${MANAGED_DOC_PATHS} 2>/dev/null\` before committing, and tell the user what was updated.
278
-
279
- If \`${bin}\` is not found, tell the user: "This project uses Caliber for agent config sync. Run /setup-caliber to get set up."
280
- `;
281
- }
282
- function hasPreCommitBlock(content) {
283
- return content.includes(BLOCK_START);
284
- }
285
- function appendPreCommitBlock(content) {
286
- if (hasPreCommitBlock(content)) return content;
287
- const trimmed = content.trimEnd();
288
- return trimmed + "\n\n" + getPreCommitBlock() + "\n";
289
- }
290
- function getCursorPreCommitRule() {
291
- return { filename: CURSOR_RULE_FILENAME, content: getCursorRuleContent() };
292
- }
293
- function hasLearningsBlock(content) {
294
- return content.includes(LEARNINGS_BLOCK_START);
295
- }
296
- function appendLearningsBlock(content) {
297
- if (hasLearningsBlock(content)) return content;
298
- const trimmed = content.trimEnd();
299
- return trimmed + "\n\n" + LEARNINGS_BLOCK + "\n";
300
- }
301
- function getCursorLearningsRule() {
302
- return { filename: CURSOR_LEARNINGS_FILENAME, content: CURSOR_LEARNINGS_CONTENT };
303
- }
304
- function getSyncBlock() {
305
- const bin = resolveCaliber();
306
- return `${SYNC_BLOCK_START}
307
- ## Context Sync
308
-
309
- This project uses [Caliber](https://github.com/caliber-ai-org/ai-setup) to keep AI agent configs in sync across Claude Code, Cursor, Copilot, and Codex.
310
- Configs update automatically before each commit via \`${bin} refresh\`.
311
- If the pre-commit hook is not set up, run \`${bin} hooks --install\` to enable automatic sync.
312
- ${SYNC_BLOCK_END}`;
313
- }
314
- function hasSyncBlock(content) {
315
- return content.includes(SYNC_BLOCK_START);
316
- }
317
- function appendSyncBlock(content) {
318
- if (hasSyncBlock(content)) return content;
319
- const trimmed = content.trimEnd();
320
- return trimmed + "\n\n" + getSyncBlock() + "\n";
321
- }
322
- function getCursorSyncContent() {
323
- const bin = resolveCaliber();
324
- return `---
325
- description: This project uses Caliber for automatic AI agent context sync
326
- alwaysApply: true
327
- ---
328
- This project uses Caliber to keep AI agent configs (CLAUDE.md, Cursor rules, Copilot instructions)
329
- in sync with the codebase automatically. Before each commit, run \`${bin} refresh\` to sync configs.
330
- If \`${bin}\` is not found, tell the user: "This project uses Caliber for agent config sync. Run /setup-caliber to get set up."
331
- `;
332
- }
333
- function getCursorSyncRule() {
334
- return { filename: CURSOR_SYNC_FILENAME, content: getCursorSyncContent() };
335
- }
336
- var BLOCK_START, BLOCK_END, MANAGED_DOC_PATHS, CURSOR_RULE_FILENAME, LEARNINGS_BLOCK_START, LEARNINGS_BLOCK_END, LEARNINGS_BLOCK, CURSOR_LEARNINGS_FILENAME, CURSOR_LEARNINGS_CONTENT, SYNC_BLOCK_START, SYNC_BLOCK_END, CURSOR_SYNC_FILENAME;
337
- var init_pre_commit_block = __esm({
338
- "src/writers/pre-commit-block.ts"() {
339
- "use strict";
340
- init_resolve_caliber();
341
- BLOCK_START = "<!-- caliber:managed:pre-commit -->";
342
- BLOCK_END = "<!-- /caliber:managed:pre-commit -->";
343
- MANAGED_DOC_PATHS = "CLAUDE.md .claude/ .cursor/ .cursorrules .github/copilot-instructions.md .github/instructions/ AGENTS.md CALIBER_LEARNINGS.md";
344
- CURSOR_RULE_FILENAME = "caliber-pre-commit.mdc";
345
- LEARNINGS_BLOCK_START = "<!-- caliber:managed:learnings -->";
346
- LEARNINGS_BLOCK_END = "<!-- /caliber:managed:learnings -->";
347
- LEARNINGS_BLOCK = `${LEARNINGS_BLOCK_START}
348
- ## Session Learnings
349
-
350
- Read \`CALIBER_LEARNINGS.md\` for patterns and anti-patterns learned from previous sessions.
351
- These are auto-extracted from real tool usage \u2014 treat them as project-specific rules.
352
- ${LEARNINGS_BLOCK_END}`;
353
- CURSOR_LEARNINGS_FILENAME = "caliber-learnings.mdc";
354
- CURSOR_LEARNINGS_CONTENT = `---
355
- description: Reference session-learned patterns from CALIBER_LEARNINGS.md
356
- alwaysApply: true
357
- ---
358
- Read \`CALIBER_LEARNINGS.md\` for patterns and anti-patterns learned from previous sessions.
359
- These are auto-extracted from real tool usage \u2014 treat them as project-specific rules.
360
- `;
361
- SYNC_BLOCK_START = "<!-- caliber:managed:sync -->";
362
- SYNC_BLOCK_END = "<!-- /caliber:managed:sync -->";
363
- CURSOR_SYNC_FILENAME = "caliber-sync.mdc";
364
- }
365
- });
366
-
367
68
  // src/lib/builtin-skills.ts
368
69
  var builtin_skills_exports = {};
369
70
  __export(builtin_skills_exports, {
@@ -375,8 +76,8 @@ __export(builtin_skills_exports, {
375
76
  buildSkillContent: () => buildSkillContent,
376
77
  ensureBuiltinSkills: () => ensureBuiltinSkills
377
78
  });
378
- import fs17 from "fs";
379
- import path16 from "path";
79
+ import fs3 from "fs";
80
+ import path3 from "path";
380
81
  function buildSkillContent(skill) {
381
82
  const frontmatter = `---
382
83
  name: ${skill.name}
@@ -541,20 +242,37 @@ grep -q "caliber" .git/hooks/pre-commit 2>/dev/null && echo "HOOK_ACTIVE" || ech
541
242
  caliber hooks --install
542
243
  \`\`\`
543
244
 
544
- ### Step 3: Check if agent configs exist
245
+ ### Step 3: Detect agents and check if configs exist
246
+
247
+ First, detect which coding agents are configured in this project:
248
+ \`\`\`bash
249
+ AGENTS=""
250
+ [ -d .claude ] && AGENTS="claude"
251
+ [ -d .cursor ] && AGENTS="\${AGENTS:+$AGENTS,}cursor"
252
+ [ -d .agents ] || [ -f AGENTS.md ] && AGENTS="\${AGENTS:+$AGENTS,}codex"
253
+ [ -f .github/copilot-instructions.md ] && AGENTS="\${AGENTS:+$AGENTS,}github-copilot"
254
+ echo "DETECTED_AGENTS=\${AGENTS:-none}"
255
+ \`\`\`
256
+
257
+ If no agents are detected, ask the user which coding agents they use (Claude Code, Cursor, Codex, GitHub Copilot).
258
+ Build the agent list from their answer as a comma-separated string (e.g. "claude,cursor").
545
259
 
260
+ Then check if agent configs exist:
546
261
  \`\`\`bash
547
262
  echo "CLAUDE_MD=$([ -f CLAUDE.md ] && echo exists || echo missing)"
548
263
  echo "CURSOR_RULES=$([ -d .cursor/rules ] && ls .cursor/rules/*.mdc 2>/dev/null | wc -l | tr -d ' ' || echo 0)"
549
264
  echo "AGENTS_MD=$([ -f AGENTS.md ] && echo exists || echo missing)"
265
+ echo "COPILOT=$([ -f .github/copilot-instructions.md ] && echo exists || echo missing)"
550
266
  \`\`\`
551
267
 
552
- - If configs exist \u2192 Tell the user which configs are present. Move to Step 4.
268
+ - If configs exist for the detected agents \u2192 Tell the user which configs are present. Move to Step 4.
553
269
  - If configs are missing \u2192 Tell the user: "No agent configs found. I'll generate them now."
270
+ Use the detected or user-selected agent list:
554
271
  \`\`\`bash
555
- caliber init --auto-approve
272
+ caliber init --auto-approve --agent <comma-separated-agents>
556
273
  \`\`\`
557
- This generates CLAUDE.md, Cursor rules, AGENTS.md, and skills for all detected agents.
274
+ For example: \`caliber init --auto-approve --agent claude,cursor\`
275
+ This generates CLAUDE.md, Cursor rules, AGENTS.md, skills, and sync infrastructure for the specified agents.
558
276
 
559
277
  ### Step 4: Check if configs are fresh
560
278
 
@@ -573,7 +291,27 @@ caliber score --json --quiet 2>/dev/null | head -1
573
291
 
574
292
  Ask the user: "Are you setting up for yourself only, or for your team too?"
575
293
 
576
- - If **solo** \u2192 Tell the user the summary (see below) and stop.
294
+ - If **solo** \u2192 Continue with solo setup:
295
+
296
+ Check if session learning is enabled:
297
+ \`\`\`bash
298
+ caliber learn status 2>/dev/null | head -3
299
+ \`\`\`
300
+ - If learning is already enabled \u2192 note it in the summary.
301
+ - If not enabled \u2192 ask the user: "Caliber can learn from your coding sessions \u2014 when you correct a mistake or fix a pattern, it remembers for next time. Enable session learning?"
302
+ If yes:
303
+ \`\`\`bash
304
+ caliber learn install
305
+ \`\`\`
306
+
307
+ Then tell the user:
308
+ "You're all set! Here's what happens next:
309
+ - Every time you commit, Caliber syncs your agent configs automatically
310
+ - Your CLAUDE.md, Cursor rules, and AGENTS.md stay current with your code
311
+ - Run \`caliber skills\` anytime to discover community skills for your stack"
312
+
313
+ Then show the summary (see below) and stop.
314
+
577
315
  - If **team** \u2192 Check if the GitHub Action already exists:
578
316
  \`\`\`bash
579
317
  [ -f .github/workflows/caliber-sync.yml ] && echo "ACTION_EXISTS" || echo "NO_ACTION"
@@ -603,7 +341,43 @@ Ask the user: "Are you setting up for yourself only, or for your team too?"
603
341
  env:
604
342
  ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
605
343
  \`\`\`
606
- Tell the user: "Add your ANTHROPIC_API_KEY to repo secrets (Settings \u2192 Secrets \u2192 Actions), then commit and push."
344
+ Now determine which LLM provider the team uses. Check the local Caliber config:
345
+ \`\`\`bash
346
+ caliber config --show 2>/dev/null || echo "NO_CONFIG"
347
+ \`\`\`
348
+
349
+ Based on the provider, the GitHub Action needs the corresponding secret:
350
+ - **anthropic** \u2192 \`ANTHROPIC_API_KEY\`
351
+ - **openai** \u2192 \`OPENAI_API_KEY\`
352
+ - **vertex** \u2192 \`VERTEX_PROJECT_ID\` and \`GOOGLE_APPLICATION_CREDENTIALS\` (service account JSON)
353
+
354
+ Update the workflow env block to match the provider. For example, if using OpenAI:
355
+ \`\`\`yaml
356
+ env:
357
+ OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
358
+ \`\`\`
359
+
360
+ Then check if the \`gh\` CLI is available to set the secret:
361
+ \`\`\`bash
362
+ command -v gh >/dev/null 2>&1 && echo "GH_AVAILABLE" || echo "NO_GH"
363
+ \`\`\`
364
+
365
+ - If GH_AVAILABLE \u2192 Ask the user for their API key and set it:
366
+ \`\`\`bash
367
+ gh secret set ANTHROPIC_API_KEY
368
+ \`\`\`
369
+ (This prompts for the value securely via stdin)
370
+ - If NO_GH \u2192 Tell the user exactly what to do:
371
+ "Go to your repo on GitHub \u2192 Settings \u2192 Secrets and variables \u2192 Actions \u2192 New repository secret.
372
+ Name: ANTHROPIC_API_KEY (or OPENAI_API_KEY depending on provider)
373
+ Value: your API key"
374
+
375
+ Finally, offer to commit and push the workflow file:
376
+ \`\`\`bash
377
+ git add .github/workflows/caliber-sync.yml
378
+ git commit -m "feat: add Caliber sync GitHub Action"
379
+ git push
380
+ \`\`\`
607
381
 
608
382
  ### Summary
609
383
 
@@ -632,52 +406,402 @@ From now on, every commit keeps all your agent configs in sync automatically.
632
406
  - The user is in the middle of time-sensitive work
633
407
  `;
634
408
  }
635
- function ensureBuiltinSkills() {
636
- const written = [];
637
- for (const { platformDir, skillsDir } of PLATFORM_CONFIGS) {
638
- if (!fs17.existsSync(platformDir)) continue;
639
- for (const skill of BUILTIN_SKILLS) {
640
- const skillPath = path16.join(skillsDir, skill.name, "SKILL.md");
641
- fs17.mkdirSync(path16.dirname(skillPath), { recursive: true });
642
- fs17.writeFileSync(skillPath, buildSkillContent(skill));
643
- written.push(skillPath);
409
+ function ensureBuiltinSkills() {
410
+ const written = [];
411
+ for (const { platformDir, skillsDir } of PLATFORM_CONFIGS) {
412
+ if (!fs3.existsSync(platformDir)) continue;
413
+ for (const skill of BUILTIN_SKILLS) {
414
+ const skillPath = path3.join(skillsDir, skill.name, "SKILL.md");
415
+ fs3.mkdirSync(path3.dirname(skillPath), { recursive: true });
416
+ fs3.writeFileSync(skillPath, buildSkillContent(skill));
417
+ written.push(skillPath);
418
+ }
419
+ }
420
+ return written;
421
+ }
422
+ var FIND_SKILLS_SKILL, SAVE_LEARNING_SKILL, SETUP_CALIBER_SKILL, BUILTIN_SKILLS, PLATFORM_CONFIGS, BUILTIN_SKILL_NAMES;
423
+ var init_builtin_skills = __esm({
424
+ "src/lib/builtin-skills.ts"() {
425
+ "use strict";
426
+ init_resolve_caliber();
427
+ FIND_SKILLS_SKILL = {
428
+ name: "find-skills",
429
+ description: "Discovers and installs community skills from the public registry. Use when the user mentions a technology, framework, or task that could benefit from specialized skills not yet installed, asks 'how do I do X', 'find a skill for X', or starts work in a new technology area. Proactively suggest when the user's task involves tools or frameworks without existing skills.",
430
+ get content() {
431
+ return getFindSkillsContent();
432
+ }
433
+ };
434
+ SAVE_LEARNING_SKILL = {
435
+ name: "save-learning",
436
+ description: "Saves user instructions as persistent learnings for future sessions. Use when the user says 'remember this', 'always do X', 'from now on', 'never do Y', or gives any instruction they want persisted across sessions. Proactively suggest when the user states a preference, convention, or rule they clearly want followed in the future.",
437
+ get content() {
438
+ return getSaveLearningContent();
439
+ }
440
+ };
441
+ SETUP_CALIBER_SKILL = {
442
+ name: "setup-caliber",
443
+ description: "Sets up Caliber for automatic AI agent context sync. Installs pre-commit hooks so CLAUDE.md, Cursor rules, and Copilot instructions update automatically on every commit. Use when Caliber hooks are not yet installed or when the user asks about keeping agent configs in sync.",
444
+ get content() {
445
+ return getSetupCaliberContent();
446
+ }
447
+ };
448
+ BUILTIN_SKILLS = [FIND_SKILLS_SKILL, SAVE_LEARNING_SKILL, SETUP_CALIBER_SKILL];
449
+ PLATFORM_CONFIGS = [
450
+ { platformDir: ".claude", skillsDir: path3.join(".claude", "skills") },
451
+ { platformDir: ".cursor", skillsDir: path3.join(".cursor", "skills") },
452
+ { platformDir: ".agents", skillsDir: path3.join(".agents", "skills") }
453
+ ];
454
+ BUILTIN_SKILL_NAMES = new Set(BUILTIN_SKILLS.map((s) => s.name));
455
+ }
456
+ });
457
+
458
+ // src/writers/pre-commit-block.ts
459
+ var pre_commit_block_exports = {};
460
+ __export(pre_commit_block_exports, {
461
+ appendLearningsBlock: () => appendLearningsBlock,
462
+ appendManagedBlocks: () => appendManagedBlocks,
463
+ appendPreCommitBlock: () => appendPreCommitBlock,
464
+ appendSyncBlock: () => appendSyncBlock,
465
+ getCursorLearningsRule: () => getCursorLearningsRule,
466
+ getCursorPreCommitRule: () => getCursorPreCommitRule,
467
+ getCursorSetupRule: () => getCursorSetupRule,
468
+ getCursorSyncRule: () => getCursorSyncRule,
469
+ hasLearningsBlock: () => hasLearningsBlock,
470
+ hasPreCommitBlock: () => hasPreCommitBlock,
471
+ hasSyncBlock: () => hasSyncBlock
472
+ });
473
+ function getCaliberNotFoundFallback(platform, bin) {
474
+ switch (platform) {
475
+ case "claude":
476
+ return `If \`${bin}\` is not found, tell the user: "This project uses Caliber for agent config sync. Run /setup-caliber to get set up."`;
477
+ case "codex":
478
+ return `If \`${bin}\` is not found, read \`.agents/skills/setup-caliber/SKILL.md\` and follow its instructions to install Caliber.`;
479
+ case "copilot":
480
+ return `If \`${bin}\` is not found, install it by running \`npm install -g @rely-ai/caliber\`, then run \`caliber hooks --install\` and \`caliber refresh\`.`;
481
+ }
482
+ }
483
+ function getPreCommitBlock(platform = "claude") {
484
+ const bin = resolveCaliber();
485
+ return `${BLOCK_START}
486
+ ## Before Committing
487
+
488
+ **IMPORTANT:** Before every git commit, you MUST ensure Caliber syncs agent configs with the latest code changes.
489
+
490
+ First, check if the pre-commit hook is already installed:
491
+ \`\`\`bash
492
+ grep -q "caliber" .git/hooks/pre-commit 2>/dev/null && echo "hook-active" || echo "no-hook"
493
+ \`\`\`
494
+
495
+ - If **hook-active**: the hook handles sync automatically \u2014 just commit normally. Tell the user: "Caliber will sync your agent configs automatically via the pre-commit hook."
496
+ - If **no-hook**: run Caliber manually before committing:
497
+ 1. Tell the user: "Caliber: Syncing agent configs with your latest changes..."
498
+ 2. Run: \`${bin} refresh && git add ${MANAGED_DOC_PATHS} 2>/dev/null\`
499
+ 3. After it completes, briefly tell the user what Caliber updated. Then proceed with the commit.
500
+
501
+ ${getCaliberNotFoundFallback(platform, bin)}
502
+ ${BLOCK_END}`;
503
+ }
504
+ function getCursorRuleContent() {
505
+ const bin = resolveCaliber();
506
+ return `---
507
+ description: Run Caliber sync before git commits to keep agent configs up to date
508
+ alwaysApply: true
509
+ ---
510
+ Before every git commit, check if the Caliber pre-commit hook is installed:
511
+ Run: \`grep -q "caliber" .git/hooks/pre-commit 2>/dev/null && echo "hook-active" || echo "no-hook"\`
512
+
513
+ If hook-active: just commit normally \u2014 Caliber syncs automatically via the hook. Tell the user: "Caliber will sync your agent configs automatically."
514
+ If no-hook: run \`${bin} refresh && git add ${MANAGED_DOC_PATHS} 2>/dev/null\` before committing, and tell the user what was updated.
515
+
516
+ If \`${bin}\` is not found, read the setup-caliber skill from .cursor/skills/setup-caliber/SKILL.md and follow its instructions to install Caliber.
517
+ `;
518
+ }
519
+ function hasPreCommitBlock(content) {
520
+ return content.includes(BLOCK_START);
521
+ }
522
+ function appendPreCommitBlock(content, platform = "claude") {
523
+ if (hasPreCommitBlock(content)) return content;
524
+ const trimmed = content.trimEnd();
525
+ return trimmed + "\n\n" + getPreCommitBlock(platform) + "\n";
526
+ }
527
+ function getCursorPreCommitRule() {
528
+ return { filename: CURSOR_RULE_FILENAME, content: getCursorRuleContent() };
529
+ }
530
+ function hasLearningsBlock(content) {
531
+ return content.includes(LEARNINGS_BLOCK_START);
532
+ }
533
+ function appendLearningsBlock(content) {
534
+ if (hasLearningsBlock(content)) return content;
535
+ const trimmed = content.trimEnd();
536
+ return trimmed + "\n\n" + LEARNINGS_BLOCK + "\n";
537
+ }
538
+ function getCursorLearningsRule() {
539
+ return { filename: CURSOR_LEARNINGS_FILENAME, content: CURSOR_LEARNINGS_CONTENT };
540
+ }
541
+ function getSyncSetupInstruction(platform, bin) {
542
+ switch (platform) {
543
+ case "claude":
544
+ return `If the pre-commit hook is not set up, run \`/setup-caliber\` to configure everything automatically.`;
545
+ case "codex":
546
+ return `If the pre-commit hook is not set up, read \`.agents/skills/setup-caliber/SKILL.md\` and follow the setup instructions.`;
547
+ case "copilot":
548
+ return `If the pre-commit hook is not set up, install it by running:
549
+ \`\`\`bash
550
+ npm install -g @rely-ai/caliber
551
+ caliber hooks --install
552
+ caliber refresh
553
+ git add ${MANAGED_DOC_PATHS} 2>/dev/null
554
+ \`\`\``;
555
+ }
556
+ }
557
+ function getSyncBlock(platform = "claude") {
558
+ const bin = resolveCaliber();
559
+ return `${SYNC_BLOCK_START}
560
+ ## Context Sync
561
+
562
+ This project uses [Caliber](https://github.com/caliber-ai-org/ai-setup) to keep AI agent configs in sync across Claude Code, Cursor, Copilot, and Codex.
563
+ Configs update automatically before each commit via \`${bin} refresh\`.
564
+ ${getSyncSetupInstruction(platform, bin)}
565
+ ${SYNC_BLOCK_END}`;
566
+ }
567
+ function hasSyncBlock(content) {
568
+ return content.includes(SYNC_BLOCK_START);
569
+ }
570
+ function appendSyncBlock(content, platform = "claude") {
571
+ if (hasSyncBlock(content)) return content;
572
+ const trimmed = content.trimEnd();
573
+ return trimmed + "\n\n" + getSyncBlock(platform) + "\n";
574
+ }
575
+ function appendManagedBlocks(content, platform = "claude") {
576
+ return appendSyncBlock(appendLearningsBlock(appendPreCommitBlock(content, platform)), platform);
577
+ }
578
+ function getCursorSyncContent() {
579
+ const bin = resolveCaliber();
580
+ return `---
581
+ description: This project uses Caliber for automatic AI agent context sync
582
+ alwaysApply: true
583
+ ---
584
+ This project uses Caliber to keep AI agent configs (CLAUDE.md, Cursor rules, Copilot instructions)
585
+ in sync with the codebase automatically. Before each commit, run \`${bin} refresh\` to sync configs.
586
+ If \`${bin}\` is not found, read the setup-caliber skill from .cursor/skills/setup-caliber/SKILL.md and follow its instructions to install Caliber.
587
+ `;
588
+ }
589
+ function getCursorSyncRule() {
590
+ return { filename: CURSOR_SYNC_FILENAME, content: getCursorSyncContent() };
591
+ }
592
+ function getCursorSetupContent() {
593
+ return `---
594
+ description: Check if Caliber agent config sync is set up and offer installation on new sessions
595
+ alwaysApply: true
596
+ ---
597
+ This project uses Caliber to keep AI agent configs in sync automatically.
598
+ At the start of each session, silently check if sync is active:
599
+
600
+ Run: \`grep -q "caliber" .git/hooks/pre-commit 2>/dev/null && echo "SYNC_ACTIVE" || echo "NO_SYNC"\`
601
+
602
+ If SYNC_ACTIVE: No action needed \u2014 Caliber syncs automatically on every commit.
603
+ If NO_SYNC: Tell the user: "This project uses Caliber for automatic agent config sync, but it's not set up on your machine yet. Want me to set it up? It takes about 30 seconds."
604
+
605
+ If the user agrees, read \`.cursor/skills/setup-caliber/SKILL.md\` and follow its instructions step by step.
606
+ `;
607
+ }
608
+ function getCursorSetupRule() {
609
+ return { filename: CURSOR_SETUP_FILENAME, content: getCursorSetupContent() };
610
+ }
611
+ var BLOCK_START, BLOCK_END, MANAGED_DOC_PATHS, CURSOR_RULE_FILENAME, LEARNINGS_BLOCK_START, LEARNINGS_BLOCK_END, LEARNINGS_BLOCK, CURSOR_LEARNINGS_FILENAME, CURSOR_LEARNINGS_CONTENT, SYNC_BLOCK_START, SYNC_BLOCK_END, CURSOR_SYNC_FILENAME, CURSOR_SETUP_FILENAME;
612
+ var init_pre_commit_block = __esm({
613
+ "src/writers/pre-commit-block.ts"() {
614
+ "use strict";
615
+ init_resolve_caliber();
616
+ BLOCK_START = "<!-- caliber:managed:pre-commit -->";
617
+ BLOCK_END = "<!-- /caliber:managed:pre-commit -->";
618
+ MANAGED_DOC_PATHS = "CLAUDE.md .claude/ .cursor/ .cursorrules .github/copilot-instructions.md .github/instructions/ AGENTS.md CALIBER_LEARNINGS.md";
619
+ CURSOR_RULE_FILENAME = "caliber-pre-commit.mdc";
620
+ LEARNINGS_BLOCK_START = "<!-- caliber:managed:learnings -->";
621
+ LEARNINGS_BLOCK_END = "<!-- /caliber:managed:learnings -->";
622
+ LEARNINGS_BLOCK = `${LEARNINGS_BLOCK_START}
623
+ ## Session Learnings
624
+
625
+ Read \`CALIBER_LEARNINGS.md\` for patterns and anti-patterns learned from previous sessions.
626
+ These are auto-extracted from real tool usage \u2014 treat them as project-specific rules.
627
+ ${LEARNINGS_BLOCK_END}`;
628
+ CURSOR_LEARNINGS_FILENAME = "caliber-learnings.mdc";
629
+ CURSOR_LEARNINGS_CONTENT = `---
630
+ description: Reference session-learned patterns from CALIBER_LEARNINGS.md
631
+ alwaysApply: true
632
+ ---
633
+ Read \`CALIBER_LEARNINGS.md\` for patterns and anti-patterns learned from previous sessions.
634
+ These are auto-extracted from real tool usage \u2014 treat them as project-specific rules.
635
+ `;
636
+ SYNC_BLOCK_START = "<!-- caliber:managed:sync -->";
637
+ SYNC_BLOCK_END = "<!-- /caliber:managed:sync -->";
638
+ CURSOR_SYNC_FILENAME = "caliber-sync.mdc";
639
+ CURSOR_SETUP_FILENAME = "caliber-setup.mdc";
640
+ }
641
+ });
642
+
643
+ // src/llm/config.ts
644
+ var config_exports = {};
645
+ __export(config_exports, {
646
+ DEFAULT_FAST_MODELS: () => DEFAULT_FAST_MODELS,
647
+ DEFAULT_MODELS: () => DEFAULT_MODELS,
648
+ MODEL_CONTEXT_WINDOWS: () => MODEL_CONTEXT_WINDOWS,
649
+ getConfigFilePath: () => getConfigFilePath,
650
+ getDisplayModel: () => getDisplayModel,
651
+ getFastModel: () => getFastModel,
652
+ getMaxPromptTokens: () => getMaxPromptTokens,
653
+ loadConfig: () => loadConfig,
654
+ readConfigFile: () => readConfigFile,
655
+ resolveFromEnv: () => resolveFromEnv,
656
+ writeConfigFile: () => writeConfigFile
657
+ });
658
+ import fs6 from "fs";
659
+ import path6 from "path";
660
+ import os2 from "os";
661
+ function getMaxPromptTokens() {
662
+ const config = loadConfig();
663
+ const model = process.env.CALIBER_MODEL || config?.model;
664
+ const contextWindow = model ? MODEL_CONTEXT_WINDOWS[model] ?? DEFAULT_CONTEXT_WINDOW : DEFAULT_CONTEXT_WINDOW;
665
+ const budget = Math.floor(contextWindow * INPUT_BUDGET_FRACTION);
666
+ return Math.max(MIN_PROMPT_TOKENS, Math.min(budget, MAX_PROMPT_TOKENS_CAP));
667
+ }
668
+ function loadConfig() {
669
+ const envConfig = resolveFromEnv();
670
+ if (envConfig) return envConfig;
671
+ return readConfigFile();
672
+ }
673
+ function resolveFromEnv() {
674
+ if (process.env.ANTHROPIC_API_KEY) {
675
+ return {
676
+ provider: "anthropic",
677
+ apiKey: process.env.ANTHROPIC_API_KEY,
678
+ model: process.env.CALIBER_MODEL || DEFAULT_MODELS.anthropic
679
+ };
680
+ }
681
+ if (process.env.VERTEX_PROJECT_ID || process.env.GCP_PROJECT_ID) {
682
+ return {
683
+ provider: "vertex",
684
+ model: process.env.CALIBER_MODEL || DEFAULT_MODELS.vertex,
685
+ vertexProjectId: process.env.VERTEX_PROJECT_ID || process.env.GCP_PROJECT_ID,
686
+ vertexRegion: process.env.VERTEX_REGION || process.env.GCP_REGION || "us-east5",
687
+ vertexCredentials: process.env.VERTEX_SA_CREDENTIALS || process.env.GOOGLE_APPLICATION_CREDENTIALS
688
+ };
689
+ }
690
+ if (process.env.OPENAI_API_KEY) {
691
+ return {
692
+ provider: "openai",
693
+ apiKey: process.env.OPENAI_API_KEY,
694
+ model: process.env.CALIBER_MODEL || DEFAULT_MODELS.openai,
695
+ baseUrl: process.env.OPENAI_BASE_URL
696
+ };
697
+ }
698
+ if (process.env.CALIBER_USE_CURSOR_SEAT === "1" || process.env.CALIBER_USE_CURSOR_SEAT === "true") {
699
+ return {
700
+ provider: "cursor",
701
+ model: process.env.CALIBER_MODEL || DEFAULT_MODELS.cursor
702
+ };
703
+ }
704
+ if (process.env.CALIBER_USE_CLAUDE_CLI === "1" || process.env.CALIBER_USE_CLAUDE_CLI === "true") {
705
+ return {
706
+ provider: "claude-cli",
707
+ model: process.env.CALIBER_MODEL || DEFAULT_MODELS["claude-cli"]
708
+ };
709
+ }
710
+ return null;
711
+ }
712
+ function readConfigFile() {
713
+ try {
714
+ if (!fs6.existsSync(CONFIG_FILE)) return null;
715
+ const raw = fs6.readFileSync(CONFIG_FILE, "utf-8");
716
+ const parsed = JSON.parse(raw);
717
+ if (!parsed.provider || !["anthropic", "vertex", "openai", "cursor", "claude-cli"].includes(parsed.provider)) {
718
+ return null;
644
719
  }
720
+ return parsed;
721
+ } catch {
722
+ return null;
645
723
  }
646
- return written;
647
724
  }
648
- var FIND_SKILLS_SKILL, SAVE_LEARNING_SKILL, SETUP_CALIBER_SKILL, BUILTIN_SKILLS, PLATFORM_CONFIGS, BUILTIN_SKILL_NAMES;
649
- var init_builtin_skills = __esm({
650
- "src/lib/builtin-skills.ts"() {
725
+ function writeConfigFile(config) {
726
+ if (!fs6.existsSync(CONFIG_DIR)) {
727
+ fs6.mkdirSync(CONFIG_DIR, { recursive: true });
728
+ }
729
+ const sanitized = { ...config };
730
+ if (sanitized.apiKey) {
731
+ sanitized.apiKey = sanitized.apiKey.trim();
732
+ }
733
+ fs6.writeFileSync(CONFIG_FILE, JSON.stringify(sanitized, null, 2) + "\n", { mode: 384 });
734
+ }
735
+ function getConfigFilePath() {
736
+ return CONFIG_FILE;
737
+ }
738
+ function getDisplayModel(config) {
739
+ if (config.model === "default" && config.provider === "claude-cli") {
740
+ return process.env.ANTHROPIC_MODEL || "default (inherited from Claude Code)";
741
+ }
742
+ return config.model;
743
+ }
744
+ function getFastModel() {
745
+ if (process.env.CALIBER_FAST_MODEL) return process.env.CALIBER_FAST_MODEL;
746
+ const config = loadConfig();
747
+ const provider = config?.provider;
748
+ if (process.env.ANTHROPIC_SMALL_FAST_MODEL && (!provider || provider === "anthropic" || provider === "vertex" || provider === "claude-cli")) {
749
+ return process.env.ANTHROPIC_SMALL_FAST_MODEL;
750
+ }
751
+ if (config?.fastModel) return config.fastModel;
752
+ if (provider) return DEFAULT_FAST_MODELS[provider];
753
+ return void 0;
754
+ }
755
+ var CONFIG_DIR, CONFIG_FILE, DEFAULT_MODELS, MODEL_CONTEXT_WINDOWS, DEFAULT_CONTEXT_WINDOW, INPUT_BUDGET_FRACTION, MAX_PROMPT_TOKENS_CAP, MIN_PROMPT_TOKENS, DEFAULT_FAST_MODELS;
756
+ var init_config = __esm({
757
+ "src/llm/config.ts"() {
651
758
  "use strict";
652
- init_resolve_caliber();
653
- FIND_SKILLS_SKILL = {
654
- name: "find-skills",
655
- description: "Discovers and installs community skills from the public registry. Use when the user mentions a technology, framework, or task that could benefit from specialized skills not yet installed, asks 'how do I do X', 'find a skill for X', or starts work in a new technology area. Proactively suggest when the user's task involves tools or frameworks without existing skills.",
656
- get content() {
657
- return getFindSkillsContent();
658
- }
759
+ CONFIG_DIR = path6.join(os2.homedir(), ".caliber");
760
+ CONFIG_FILE = path6.join(CONFIG_DIR, "config.json");
761
+ DEFAULT_MODELS = {
762
+ anthropic: "claude-sonnet-4-6",
763
+ vertex: "claude-sonnet-4-6",
764
+ openai: "gpt-4.1",
765
+ cursor: "sonnet-4.6",
766
+ "claude-cli": "default"
659
767
  };
660
- SAVE_LEARNING_SKILL = {
661
- name: "save-learning",
662
- description: "Saves user instructions as persistent learnings for future sessions. Use when the user says 'remember this', 'always do X', 'from now on', 'never do Y', or gives any instruction they want persisted across sessions. Proactively suggest when the user states a preference, convention, or rule they clearly want followed in the future.",
663
- get content() {
664
- return getSaveLearningContent();
665
- }
768
+ MODEL_CONTEXT_WINDOWS = {
769
+ "claude-sonnet-4-6": 2e5,
770
+ "claude-opus-4-6": 2e5,
771
+ "claude-haiku-4-5-20251001": 2e5,
772
+ "claude-sonnet-4-5-20250514": 2e5,
773
+ "gpt-4.1": 1e6,
774
+ "gpt-4.1-mini": 1e6,
775
+ "gpt-4o": 128e3,
776
+ "gpt-4o-mini": 128e3,
777
+ "sonnet-4.6": 2e5
666
778
  };
667
- SETUP_CALIBER_SKILL = {
668
- name: "setup-caliber",
669
- description: "Sets up Caliber for automatic AI agent context sync. Installs pre-commit hooks so CLAUDE.md, Cursor rules, and Copilot instructions update automatically on every commit. Use when Caliber hooks are not yet installed or when the user asks about keeping agent configs in sync.",
670
- get content() {
671
- return getSetupCaliberContent();
672
- }
779
+ DEFAULT_CONTEXT_WINDOW = 2e5;
780
+ INPUT_BUDGET_FRACTION = 0.6;
781
+ MAX_PROMPT_TOKENS_CAP = 3e5;
782
+ MIN_PROMPT_TOKENS = 3e4;
783
+ DEFAULT_FAST_MODELS = {
784
+ anthropic: "claude-haiku-4-5-20251001",
785
+ vertex: "claude-haiku-4-5-20251001",
786
+ openai: "gpt-4.1-mini",
787
+ cursor: "gpt-5.3-codex-fast"
673
788
  };
674
- BUILTIN_SKILLS = [FIND_SKILLS_SKILL, SAVE_LEARNING_SKILL, SETUP_CALIBER_SKILL];
675
- PLATFORM_CONFIGS = [
676
- { platformDir: ".claude", skillsDir: path16.join(".claude", "skills") },
677
- { platformDir: ".cursor", skillsDir: path16.join(".cursor", "skills") },
678
- { platformDir: ".agents", skillsDir: path16.join(".agents", "skills") }
679
- ];
680
- BUILTIN_SKILL_NAMES = new Set(BUILTIN_SKILLS.map((s) => s.name));
789
+ }
790
+ });
791
+
792
+ // src/llm/types.ts
793
+ var types_exports = {};
794
+ __export(types_exports, {
795
+ isSeatBased: () => isSeatBased
796
+ });
797
+ function isSeatBased(provider) {
798
+ return SEAT_BASED_PROVIDERS.has(provider);
799
+ }
800
+ var SEAT_BASED_PROVIDERS;
801
+ var init_types = __esm({
802
+ "src/llm/types.ts"() {
803
+ "use strict";
804
+ SEAT_BASED_PROVIDERS = /* @__PURE__ */ new Set(["cursor", "claude-cli"]);
681
805
  }
682
806
  });
683
807
 
@@ -1006,8 +1130,8 @@ import chalk14 from "chalk";
1006
1130
  import fs33 from "fs";
1007
1131
 
1008
1132
  // src/fingerprint/index.ts
1009
- import fs8 from "fs";
1010
- import path7 from "path";
1133
+ import fs9 from "fs";
1134
+ import path8 from "path";
1011
1135
 
1012
1136
  // src/fingerprint/git.ts
1013
1137
  import { execSync } from "child_process";
@@ -1115,8 +1239,8 @@ function scan(base, rel, depth, maxDepth, result) {
1115
1239
  }
1116
1240
 
1117
1241
  // src/fingerprint/existing-config.ts
1118
- import fs2 from "fs";
1119
- import path3 from "path";
1242
+ import fs4 from "fs";
1243
+ import path4 from "path";
1120
1244
 
1121
1245
  // src/constants.ts
1122
1246
  import path2 from "path";
@@ -1151,97 +1275,121 @@ var LEARNING_LAST_ERROR_FILE = "last-error.json";
1151
1275
  var MIN_SESSIONS_FOR_COMPARISON = 3;
1152
1276
 
1153
1277
  // src/fingerprint/existing-config.ts
1278
+ init_builtin_skills();
1279
+ init_pre_commit_block();
1280
+ var MANAGED_CURSOR_RULES = /* @__PURE__ */ new Set([
1281
+ getCursorPreCommitRule().filename,
1282
+ getCursorLearningsRule().filename,
1283
+ getCursorSyncRule().filename,
1284
+ getCursorSetupRule().filename
1285
+ ]);
1154
1286
  function readExistingConfigs(dir) {
1155
1287
  const configs = {};
1156
- const readmeMdPath = path3.join(dir, "README.md");
1157
- if (fs2.existsSync(readmeMdPath)) {
1158
- configs.readmeMd = fs2.readFileSync(readmeMdPath, "utf-8");
1288
+ const readmeMdPath = path4.join(dir, "README.md");
1289
+ if (fs4.existsSync(readmeMdPath)) {
1290
+ configs.readmeMd = fs4.readFileSync(readmeMdPath, "utf-8");
1159
1291
  }
1160
- const agentsMdPath = path3.join(dir, "AGENTS.md");
1161
- if (fs2.existsSync(agentsMdPath)) {
1162
- configs.agentsMd = fs2.readFileSync(agentsMdPath, "utf-8");
1292
+ const agentsMdPath = path4.join(dir, "AGENTS.md");
1293
+ if (fs4.existsSync(agentsMdPath)) {
1294
+ configs.agentsMd = fs4.readFileSync(agentsMdPath, "utf-8");
1163
1295
  }
1164
- const claudeMdPath = path3.join(dir, "CLAUDE.md");
1165
- if (fs2.existsSync(claudeMdPath)) {
1166
- configs.claudeMd = fs2.readFileSync(claudeMdPath, "utf-8");
1296
+ const claudeMdPath = path4.join(dir, "CLAUDE.md");
1297
+ if (fs4.existsSync(claudeMdPath)) {
1298
+ configs.claudeMd = fs4.readFileSync(claudeMdPath, "utf-8");
1167
1299
  }
1168
- const claudeSettingsPath = path3.join(dir, ".claude", "settings.json");
1169
- if (fs2.existsSync(claudeSettingsPath)) {
1300
+ const claudeSettingsPath = path4.join(dir, ".claude", "settings.json");
1301
+ if (fs4.existsSync(claudeSettingsPath)) {
1170
1302
  try {
1171
- configs.claudeSettings = JSON.parse(fs2.readFileSync(claudeSettingsPath, "utf-8"));
1303
+ configs.claudeSettings = JSON.parse(fs4.readFileSync(claudeSettingsPath, "utf-8"));
1172
1304
  } catch {
1173
1305
  }
1174
1306
  }
1175
- const skillsDir = path3.join(dir, ".claude", "skills");
1176
- if (fs2.existsSync(skillsDir)) {
1307
+ const skillsDir = path4.join(dir, ".claude", "skills");
1308
+ if (fs4.existsSync(skillsDir)) {
1177
1309
  try {
1178
- const entries = fs2.readdirSync(skillsDir);
1310
+ const entries = fs4.readdirSync(skillsDir);
1179
1311
  const skills = [];
1180
1312
  for (const entry of entries) {
1181
- const entryPath = path3.join(skillsDir, entry);
1182
- const skillMdPath = path3.join(entryPath, "SKILL.md");
1183
- if (fs2.statSync(entryPath).isDirectory() && fs2.existsSync(skillMdPath)) {
1184
- skills.push({ filename: `${entry}/SKILL.md`, content: fs2.readFileSync(skillMdPath, "utf-8") });
1313
+ if (BUILTIN_SKILL_NAMES.has(entry)) continue;
1314
+ const entryPath = path4.join(skillsDir, entry);
1315
+ const skillMdPath = path4.join(entryPath, "SKILL.md");
1316
+ if (fs4.statSync(entryPath).isDirectory() && fs4.existsSync(skillMdPath)) {
1317
+ skills.push({ filename: `${entry}/SKILL.md`, content: fs4.readFileSync(skillMdPath, "utf-8") });
1185
1318
  } else if (entry.endsWith(".md")) {
1186
- skills.push({ filename: entry, content: fs2.readFileSync(entryPath, "utf-8") });
1319
+ skills.push({ filename: entry, content: fs4.readFileSync(entryPath, "utf-8") });
1187
1320
  }
1188
1321
  }
1189
1322
  if (skills.length > 0) configs.claudeSkills = skills;
1190
1323
  } catch {
1191
1324
  }
1192
1325
  }
1193
- const cursorrulesPath = path3.join(dir, ".cursorrules");
1194
- if (fs2.existsSync(cursorrulesPath)) {
1195
- configs.cursorrules = fs2.readFileSync(cursorrulesPath, "utf-8");
1326
+ const cursorrulesPath = path4.join(dir, ".cursorrules");
1327
+ if (fs4.existsSync(cursorrulesPath)) {
1328
+ configs.cursorrules = fs4.readFileSync(cursorrulesPath, "utf-8");
1196
1329
  }
1197
- const cursorRulesDir = path3.join(dir, ".cursor", "rules");
1198
- if (fs2.existsSync(cursorRulesDir)) {
1330
+ const cursorRulesDir = path4.join(dir, ".cursor", "rules");
1331
+ if (fs4.existsSync(cursorRulesDir)) {
1199
1332
  try {
1200
- const files = fs2.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"));
1333
+ const files = fs4.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc") && !MANAGED_CURSOR_RULES.has(f));
1201
1334
  configs.cursorRules = files.map((f) => ({
1202
1335
  filename: f,
1203
- content: fs2.readFileSync(path3.join(cursorRulesDir, f), "utf-8")
1336
+ content: fs4.readFileSync(path4.join(cursorRulesDir, f), "utf-8")
1204
1337
  }));
1205
1338
  } catch {
1206
1339
  }
1207
1340
  }
1208
- const cursorSkillsDir = path3.join(dir, ".cursor", "skills");
1209
- if (fs2.existsSync(cursorSkillsDir)) {
1341
+ const cursorSkillsDir = path4.join(dir, ".cursor", "skills");
1342
+ if (fs4.existsSync(cursorSkillsDir)) {
1210
1343
  try {
1211
- const slugs = fs2.readdirSync(cursorSkillsDir).filter((f) => {
1212
- return fs2.statSync(path3.join(cursorSkillsDir, f)).isDirectory();
1344
+ const slugs = fs4.readdirSync(cursorSkillsDir).filter((f) => {
1345
+ return fs4.statSync(path4.join(cursorSkillsDir, f)).isDirectory() && !BUILTIN_SKILL_NAMES.has(f);
1213
1346
  });
1214
- configs.cursorSkills = slugs.filter((slug) => fs2.existsSync(path3.join(cursorSkillsDir, slug, "SKILL.md"))).map((name) => ({
1347
+ configs.cursorSkills = slugs.filter((slug) => fs4.existsSync(path4.join(cursorSkillsDir, slug, "SKILL.md"))).map((name) => ({
1215
1348
  name,
1216
1349
  filename: "SKILL.md",
1217
- content: fs2.readFileSync(path3.join(cursorSkillsDir, name, "SKILL.md"), "utf-8")
1350
+ content: fs4.readFileSync(path4.join(cursorSkillsDir, name, "SKILL.md"), "utf-8")
1218
1351
  }));
1219
1352
  } catch {
1220
1353
  }
1221
1354
  }
1222
- const mcpJsonPath = path3.join(dir, ".mcp.json");
1223
- if (fs2.existsSync(mcpJsonPath)) {
1355
+ const mcpJsonPath = path4.join(dir, ".mcp.json");
1356
+ if (fs4.existsSync(mcpJsonPath)) {
1224
1357
  try {
1225
- const mcpJson = JSON.parse(fs2.readFileSync(mcpJsonPath, "utf-8"));
1358
+ const mcpJson = JSON.parse(fs4.readFileSync(mcpJsonPath, "utf-8"));
1226
1359
  if (mcpJson.mcpServers) {
1227
1360
  configs.claudeMcpServers = mcpJson.mcpServers;
1228
1361
  }
1229
1362
  } catch {
1230
1363
  }
1231
1364
  }
1232
- const cursorMcpPath = path3.join(dir, ".cursor", "mcp.json");
1233
- if (fs2.existsSync(cursorMcpPath)) {
1365
+ const cursorMcpPath = path4.join(dir, ".cursor", "mcp.json");
1366
+ if (fs4.existsSync(cursorMcpPath)) {
1234
1367
  try {
1235
- const cursorMcpJson = JSON.parse(fs2.readFileSync(cursorMcpPath, "utf-8"));
1368
+ const cursorMcpJson = JSON.parse(fs4.readFileSync(cursorMcpPath, "utf-8"));
1236
1369
  if (cursorMcpJson.mcpServers) {
1237
1370
  configs.cursorMcpServers = cursorMcpJson.mcpServers;
1238
1371
  }
1239
1372
  } catch {
1240
1373
  }
1241
1374
  }
1242
- if (fs2.existsSync(PERSONAL_LEARNINGS_FILE)) {
1375
+ const copilotPath = path4.join(dir, ".github", "copilot-instructions.md");
1376
+ if (fs4.existsSync(copilotPath)) {
1377
+ configs.copilotInstructions = fs4.readFileSync(copilotPath, "utf-8");
1378
+ }
1379
+ const instructionsDir = path4.join(dir, ".github", "instructions");
1380
+ if (fs4.existsSync(instructionsDir)) {
1381
+ try {
1382
+ const files = fs4.readdirSync(instructionsDir).filter((f) => f.endsWith(".md"));
1383
+ configs.copilotInstructionFiles = files.map((f) => ({
1384
+ filename: f,
1385
+ content: fs4.readFileSync(path4.join(instructionsDir, f), "utf-8")
1386
+ }));
1387
+ } catch {
1388
+ }
1389
+ }
1390
+ if (fs4.existsSync(PERSONAL_LEARNINGS_FILE)) {
1243
1391
  try {
1244
- const content = fs2.readFileSync(PERSONAL_LEARNINGS_FILE, "utf-8");
1392
+ const content = fs4.readFileSync(PERSONAL_LEARNINGS_FILE, "utf-8");
1245
1393
  const bullets = content.split("\n").filter((l) => l.startsWith("- ")).join("\n");
1246
1394
  if (bullets) configs.personalLearnings = bullets;
1247
1395
  } catch {
@@ -1251,9 +1399,9 @@ function readExistingConfigs(dir) {
1251
1399
  }
1252
1400
 
1253
1401
  // src/fingerprint/code-analysis.ts
1254
- import fs3 from "fs";
1255
- import path4 from "path";
1256
- import { execSync as execSync3 } from "child_process";
1402
+ import fs5 from "fs";
1403
+ import path5 from "path";
1404
+ import { execSync as execSync4 } from "child_process";
1257
1405
 
1258
1406
  // src/lib/sanitize.ts
1259
1407
  var KNOWN_PREFIX_PATTERNS = [
@@ -1597,18 +1745,18 @@ function extractSkeletonIndentBased(lines, ext) {
1597
1745
  }
1598
1746
  function extractImports(content, filePath) {
1599
1747
  const imports = [];
1600
- const dir = path4.dirname(filePath);
1748
+ const dir = path5.dirname(filePath);
1601
1749
  for (const line of content.split("\n")) {
1602
1750
  const trimmed = line.trim();
1603
1751
  const jsMatch = trimmed.match(/(?:from|require\()\s*['"]([^'"]+)['"]/);
1604
1752
  if (jsMatch && jsMatch[1].startsWith(".")) {
1605
- imports.push(path4.normalize(path4.join(dir, jsMatch[1])));
1753
+ imports.push(path5.normalize(path5.join(dir, jsMatch[1])));
1606
1754
  continue;
1607
1755
  }
1608
1756
  const pyMatch = trimmed.match(/^from\s+(\.[.\w]*)\s+import/);
1609
1757
  if (pyMatch) {
1610
1758
  const modulePath = pyMatch[1].replace(/\./g, "/");
1611
- imports.push(path4.normalize(path4.join(dir, modulePath)));
1759
+ imports.push(path5.normalize(path5.join(dir, modulePath)));
1612
1760
  continue;
1613
1761
  }
1614
1762
  const goMatch = trimmed.match(/^\s*"([^"]+)"/);
@@ -1621,7 +1769,7 @@ function extractImports(content, filePath) {
1621
1769
  function buildImportCounts(files) {
1622
1770
  const counts = /* @__PURE__ */ new Map();
1623
1771
  for (const [filePath, content] of files) {
1624
- const ext = path4.extname(filePath).toLowerCase();
1772
+ const ext = path5.extname(filePath).toLowerCase();
1625
1773
  if (!SOURCE_EXTENSIONS.has(ext)) continue;
1626
1774
  const imports = extractImports(content, filePath);
1627
1775
  for (const imp of imports) {
@@ -1640,7 +1788,7 @@ function buildImportCounts(files) {
1640
1788
  function getGitFrequency(dir) {
1641
1789
  const freq = /* @__PURE__ */ new Map();
1642
1790
  try {
1643
- const output = execSync3(
1791
+ const output = execSync4(
1644
1792
  'git log --since="6 months ago" --format="" --name-only --diff-filter=ACMR 2>/dev/null | head -10000',
1645
1793
  { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
1646
1794
  );
@@ -1655,7 +1803,7 @@ function getGitFrequency(dir) {
1655
1803
  function groupByDirectory(files) {
1656
1804
  const groups = /* @__PURE__ */ new Map();
1657
1805
  for (const f of files) {
1658
- const dir = path4.dirname(f.path);
1806
+ const dir = path5.dirname(f.path);
1659
1807
  const group = groups.get(dir) || [];
1660
1808
  group.push(f);
1661
1809
  groups.set(dir, group);
@@ -1676,14 +1824,14 @@ function analyzeCode(dir) {
1676
1824
  let totalChars = 0;
1677
1825
  for (const relPath of allPaths) {
1678
1826
  try {
1679
- totalChars += fs3.statSync(path4.join(dir, relPath)).size;
1827
+ totalChars += fs5.statSync(path5.join(dir, relPath)).size;
1680
1828
  } catch {
1681
1829
  }
1682
1830
  }
1683
1831
  const fileContents = /* @__PURE__ */ new Map();
1684
1832
  for (const relPath of allPaths) {
1685
1833
  try {
1686
- const content = fs3.readFileSync(path4.join(dir, relPath), "utf-8");
1834
+ const content = fs5.readFileSync(path5.join(dir, relPath), "utf-8");
1687
1835
  if (content.split("\n").length <= 500) fileContents.set(relPath, content);
1688
1836
  } catch {
1689
1837
  }
@@ -1693,7 +1841,7 @@ function analyzeCode(dir) {
1693
1841
  const scored = [];
1694
1842
  let compressedChars = 0;
1695
1843
  for (const [relPath, rawContent] of fileContents) {
1696
- const ext = path4.extname(relPath).toLowerCase();
1844
+ const ext = path5.extname(relPath).toLowerCase();
1697
1845
  const compressed = compressContent(rawContent, ext);
1698
1846
  const skeleton = extractSkeleton(compressed, ext);
1699
1847
  compressedChars += compressed.length;
@@ -1722,7 +1870,7 @@ function analyzeCode(dir) {
1722
1870
  }
1723
1871
  if (similar.length > 0) {
1724
1872
  dupGroups++;
1725
- const names = similar.map((f) => path4.basename(f.path));
1873
+ const names = similar.map((f) => path5.basename(f.path));
1726
1874
  const summary = `(${similar.length} similar file${similar.length === 1 ? "" : "s"} in ${dirPath}/: ${names.join(", ")})`;
1727
1875
  const summarySize = summary.length + 30;
1728
1876
  if (includedChars + summarySize <= CHAR_BUDGET) {
@@ -1759,10 +1907,10 @@ function analyzeCode(dir) {
1759
1907
  }
1760
1908
  function walkDir(base, rel, depth, maxDepth, files) {
1761
1909
  if (depth > maxDepth) return;
1762
- const fullPath = path4.join(base, rel);
1910
+ const fullPath = path5.join(base, rel);
1763
1911
  let entries;
1764
1912
  try {
1765
- entries = fs3.readdirSync(fullPath, { withFileTypes: true });
1913
+ entries = fs5.readdirSync(fullPath, { withFileTypes: true });
1766
1914
  } catch {
1767
1915
  return;
1768
1916
  }
@@ -1775,7 +1923,7 @@ function walkDir(base, rel, depth, maxDepth, files) {
1775
1923
  } else if (entry.isFile()) {
1776
1924
  if (SKIP_FILES.has(entry.name)) continue;
1777
1925
  if (SKIP_PATTERNS.some((p) => p.test(entry.name))) continue;
1778
- const ext = path4.extname(entry.name).toLowerCase();
1926
+ const ext = path5.extname(entry.name).toLowerCase();
1779
1927
  if (TEXT_EXTENSIONS.has(ext) || depth === 0 && !ext && !entry.name.startsWith(".")) {
1780
1928
  files.push(relPath);
1781
1929
  }
@@ -1783,7 +1931,7 @@ function walkDir(base, rel, depth, maxDepth, files) {
1783
1931
  }
1784
1932
  }
1785
1933
  function filePriority(filePath) {
1786
- const base = path4.basename(filePath);
1934
+ const base = path5.basename(filePath);
1787
1935
  const entryPoints = /* @__PURE__ */ new Set([
1788
1936
  "index.ts",
1789
1937
  "index.js",
@@ -1915,7 +2063,7 @@ var AnthropicProvider = class {
1915
2063
  };
1916
2064
 
1917
2065
  // src/llm/vertex.ts
1918
- import fs5 from "fs";
2066
+ import fs7 from "fs";
1919
2067
  import { AnthropicVertex } from "@anthropic-ai/vertex-sdk";
1920
2068
  import { GoogleAuth } from "google-auth-library";
1921
2069
  var VertexProvider = class {
@@ -1938,7 +2086,7 @@ var VertexProvider = class {
1938
2086
  }
1939
2087
  } else {
1940
2088
  try {
1941
- creds = JSON.parse(fs5.readFileSync(raw, "utf-8"));
2089
+ creds = JSON.parse(fs7.readFileSync(raw, "utf-8"));
1942
2090
  } catch {
1943
2091
  throw new Error(`Cannot read credentials file: ${raw}`);
1944
2092
  }
@@ -3107,6 +3255,11 @@ CONSERVATIVE UPDATE means:
3107
3255
  - NEVER remove code blocks, backtick references, or architecture paths unless the diff deleted them
3108
3256
  - NEVER replace specific paths/commands with generic prose
3109
3257
 
3258
+ Cross-agent sync:
3259
+ - When a change affects CLAUDE.md, apply the same semantic update to AGENTS.md and copilot instructions if they exist
3260
+ - Each file uses its own format and conventions \u2014 do NOT copy content verbatim between them
3261
+ - The goal is consistency: all agent config files should reflect the same project state after refresh
3262
+
3110
3263
  Quality constraints (the output is scored deterministically):
3111
3264
  - CLAUDE.md / AGENTS.md: MUST stay under 400 lines. If the diff adds content, trim the least important lines elsewhere.
3112
3265
  - Keep 3+ code blocks with executable commands \u2014 do not remove code blocks
@@ -3115,16 +3268,17 @@ Quality constraints (the output is scored deterministically):
3115
3268
  - Preserve the existing structure (headings, bullet style, formatting)
3116
3269
 
3117
3270
  Managed content:
3118
- - Keep managed blocks (<!-- caliber:managed --> ... <!-- /caliber:managed -->) intact
3119
- - Keep context sync blocks (<!-- caliber:managed:sync --> ... <!-- /caliber:managed:sync -->) intact
3271
+ - All blocks between <!-- caliber:managed:* --> and <!-- /caliber:managed:* --> are managed by Caliber \u2014 copy them verbatim, do NOT modify their contents
3272
+ - This includes: pre-commit, sync, and learnings blocks
3120
3273
  - Do NOT modify CALIBER_LEARNINGS.md \u2014 it is managed separately
3121
- - Preserve any references to CALIBER_LEARNINGS.md in CLAUDE.md
3274
+ - Preserve any references to CALIBER_LEARNINGS.md
3122
3275
 
3123
3276
  Return a JSON object with this exact shape:
3124
3277
  {
3125
3278
  "updatedDocs": {
3126
3279
  "claudeMd": "<updated content or null>",
3127
3280
  "readmeMd": "<updated content or null>",
3281
+ "agentsMd": "<updated content or null>",
3128
3282
  "cursorrules": "<updated content or null>",
3129
3283
  "cursorRules": [{"filename": "name.mdc", "content": "..."}] or null,
3130
3284
  "claudeSkills": [{"filename": "name.md", "content": "..."}] or null,
@@ -3286,15 +3440,15 @@ async function detectProjectStack(fileTree, suffixCounts) {
3286
3440
  init_config();
3287
3441
 
3288
3442
  // src/fingerprint/cache.ts
3289
- import fs7 from "fs";
3290
- import path6 from "path";
3443
+ import fs8 from "fs";
3444
+ import path7 from "path";
3291
3445
  import crypto from "crypto";
3292
3446
  import { execSync as execSync7 } from "child_process";
3293
3447
  var CACHE_VERSION = 1;
3294
3448
  var CACHE_DIR = ".caliber/cache";
3295
3449
  var CACHE_FILE = "fingerprint.json";
3296
3450
  function getCachePath(dir) {
3297
- return path6.join(dir, CACHE_DIR, CACHE_FILE);
3451
+ return path7.join(dir, CACHE_DIR, CACHE_FILE);
3298
3452
  }
3299
3453
  function getGitHead(dir) {
3300
3454
  try {
@@ -3331,8 +3485,8 @@ function computeTreeSignature(fileTree, dir) {
3331
3485
  function loadFingerprintCache(dir, fileTree) {
3332
3486
  const cachePath = getCachePath(dir);
3333
3487
  try {
3334
- if (!fs7.existsSync(cachePath)) return null;
3335
- const raw = fs7.readFileSync(cachePath, "utf-8");
3488
+ if (!fs8.existsSync(cachePath)) return null;
3489
+ const raw = fs8.readFileSync(cachePath, "utf-8");
3336
3490
  const cache = JSON.parse(raw);
3337
3491
  if (cache.version !== CACHE_VERSION) return null;
3338
3492
  const currentHead = getGitHead(dir);
@@ -3353,9 +3507,9 @@ function loadFingerprintCache(dir, fileTree) {
3353
3507
  function saveFingerprintCache(dir, fileTree, codeAnalysis, languages, frameworks, tools, workspaces) {
3354
3508
  const cachePath = getCachePath(dir);
3355
3509
  try {
3356
- const cacheDir = path6.dirname(cachePath);
3357
- if (!fs7.existsSync(cacheDir)) {
3358
- fs7.mkdirSync(cacheDir, { recursive: true });
3510
+ const cacheDir = path7.dirname(cachePath);
3511
+ if (!fs8.existsSync(cacheDir)) {
3512
+ fs8.mkdirSync(cacheDir, { recursive: true });
3359
3513
  }
3360
3514
  const cache = {
3361
3515
  version: CACHE_VERSION,
@@ -3367,15 +3521,15 @@ function saveFingerprintCache(dir, fileTree, codeAnalysis, languages, frameworks
3367
3521
  tools,
3368
3522
  workspaces
3369
3523
  };
3370
- fs7.writeFileSync(cachePath, JSON.stringify(cache), "utf-8");
3524
+ fs8.writeFileSync(cachePath, JSON.stringify(cache), "utf-8");
3371
3525
  } catch {
3372
3526
  }
3373
3527
  }
3374
3528
  function getDetectedWorkspaces(dir) {
3375
3529
  const cachePath = getCachePath(dir);
3376
3530
  try {
3377
- if (!fs7.existsSync(cachePath)) return [];
3378
- const raw = fs7.readFileSync(cachePath, "utf-8");
3531
+ if (!fs8.existsSync(cachePath)) return [];
3532
+ const raw = fs8.readFileSync(cachePath, "utf-8");
3379
3533
  const cache = JSON.parse(raw);
3380
3534
  return cache.workspaces ?? [];
3381
3535
  } catch {
@@ -3427,9 +3581,9 @@ async function collectFingerprint(dir) {
3427
3581
  }
3428
3582
  function readPackageName(dir) {
3429
3583
  try {
3430
- const pkgPath = path7.join(dir, "package.json");
3431
- if (!fs8.existsSync(pkgPath)) return void 0;
3432
- const pkg3 = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
3584
+ const pkgPath = path8.join(dir, "package.json");
3585
+ if (!fs9.existsSync(pkgPath)) return void 0;
3586
+ const pkg3 = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
3433
3587
  return pkg3.name;
3434
3588
  } catch {
3435
3589
  return void 0;
@@ -3443,7 +3597,7 @@ async function enrichWithLLM(fingerprint) {
3443
3597
  const suffixCounts = {};
3444
3598
  for (const entry of fingerprint.fileTree) {
3445
3599
  if (entry.endsWith("/")) continue;
3446
- const ext = path7.extname(entry).toLowerCase();
3600
+ const ext = path8.extname(entry).toLowerCase();
3447
3601
  if (ext) {
3448
3602
  suffixCounts[ext] = (suffixCounts[ext] || 0) + 1;
3449
3603
  }
@@ -3459,22 +3613,22 @@ async function enrichWithLLM(fingerprint) {
3459
3613
  }
3460
3614
 
3461
3615
  // src/scanner/index.ts
3462
- import fs9 from "fs";
3463
- import path8 from "path";
3616
+ import fs10 from "fs";
3617
+ import path9 from "path";
3464
3618
  import crypto2 from "crypto";
3465
3619
  import os4 from "os";
3466
3620
  function detectPlatforms() {
3467
3621
  const home = os4.homedir();
3468
3622
  return {
3469
- claude: fs9.existsSync(path8.join(home, ".claude")),
3470
- cursor: fs9.existsSync(getCursorConfigDir()),
3471
- codex: fs9.existsSync(path8.join(home, ".codex"))
3623
+ claude: fs10.existsSync(path9.join(home, ".claude")),
3624
+ cursor: fs10.existsSync(getCursorConfigDir()),
3625
+ codex: fs10.existsSync(path9.join(home, ".codex"))
3472
3626
  };
3473
3627
  }
3474
3628
  function scanLocalState(dir) {
3475
3629
  const items = [];
3476
- const claudeMdPath = path8.join(dir, "CLAUDE.md");
3477
- if (fs9.existsSync(claudeMdPath)) {
3630
+ const claudeMdPath = path9.join(dir, "CLAUDE.md");
3631
+ if (fs10.existsSync(claudeMdPath)) {
3478
3632
  items.push({
3479
3633
  type: "rule",
3480
3634
  platform: "claude",
@@ -3483,10 +3637,10 @@ function scanLocalState(dir) {
3483
3637
  path: claudeMdPath
3484
3638
  });
3485
3639
  }
3486
- const skillsDir = path8.join(dir, ".claude", "skills");
3487
- if (fs9.existsSync(skillsDir)) {
3488
- for (const file of fs9.readdirSync(skillsDir).filter((f) => f.endsWith(".md"))) {
3489
- const filePath = path8.join(skillsDir, file);
3640
+ const skillsDir = path9.join(dir, ".claude", "skills");
3641
+ if (fs10.existsSync(skillsDir)) {
3642
+ for (const file of fs10.readdirSync(skillsDir).filter((f) => f.endsWith(".md"))) {
3643
+ const filePath = path9.join(skillsDir, file);
3490
3644
  items.push({
3491
3645
  type: "skill",
3492
3646
  platform: "claude",
@@ -3496,10 +3650,10 @@ function scanLocalState(dir) {
3496
3650
  });
3497
3651
  }
3498
3652
  }
3499
- const mcpJsonPath = path8.join(dir, ".mcp.json");
3500
- if (fs9.existsSync(mcpJsonPath)) {
3653
+ const mcpJsonPath = path9.join(dir, ".mcp.json");
3654
+ if (fs10.existsSync(mcpJsonPath)) {
3501
3655
  try {
3502
- const mcpJson = JSON.parse(fs9.readFileSync(mcpJsonPath, "utf-8"));
3656
+ const mcpJson = JSON.parse(fs10.readFileSync(mcpJsonPath, "utf-8"));
3503
3657
  if (mcpJson.mcpServers) {
3504
3658
  for (const name of Object.keys(mcpJson.mcpServers)) {
3505
3659
  items.push({
@@ -3515,8 +3669,8 @@ function scanLocalState(dir) {
3515
3669
  warnScanSkip(".mcp.json", error);
3516
3670
  }
3517
3671
  }
3518
- const agentsMdPath = path8.join(dir, "AGENTS.md");
3519
- if (fs9.existsSync(agentsMdPath)) {
3672
+ const agentsMdPath = path9.join(dir, "AGENTS.md");
3673
+ if (fs10.existsSync(agentsMdPath)) {
3520
3674
  items.push({
3521
3675
  type: "rule",
3522
3676
  platform: "codex",
@@ -3525,12 +3679,12 @@ function scanLocalState(dir) {
3525
3679
  path: agentsMdPath
3526
3680
  });
3527
3681
  }
3528
- const codexSkillsDir = path8.join(dir, ".agents", "skills");
3529
- if (fs9.existsSync(codexSkillsDir)) {
3682
+ const codexSkillsDir = path9.join(dir, ".agents", "skills");
3683
+ if (fs10.existsSync(codexSkillsDir)) {
3530
3684
  try {
3531
- for (const name of fs9.readdirSync(codexSkillsDir)) {
3532
- const skillFile = path8.join(codexSkillsDir, name, "SKILL.md");
3533
- if (fs9.existsSync(skillFile)) {
3685
+ for (const name of fs10.readdirSync(codexSkillsDir)) {
3686
+ const skillFile = path9.join(codexSkillsDir, name, "SKILL.md");
3687
+ if (fs10.existsSync(skillFile)) {
3534
3688
  items.push({
3535
3689
  type: "skill",
3536
3690
  platform: "codex",
@@ -3544,8 +3698,8 @@ function scanLocalState(dir) {
3544
3698
  warnScanSkip(".agents/skills", error);
3545
3699
  }
3546
3700
  }
3547
- const cursorrulesPath = path8.join(dir, ".cursorrules");
3548
- if (fs9.existsSync(cursorrulesPath)) {
3701
+ const cursorrulesPath = path9.join(dir, ".cursorrules");
3702
+ if (fs10.existsSync(cursorrulesPath)) {
3549
3703
  items.push({
3550
3704
  type: "rule",
3551
3705
  platform: "cursor",
@@ -3554,10 +3708,10 @@ function scanLocalState(dir) {
3554
3708
  path: cursorrulesPath
3555
3709
  });
3556
3710
  }
3557
- const cursorRulesDir = path8.join(dir, ".cursor", "rules");
3558
- if (fs9.existsSync(cursorRulesDir)) {
3559
- for (const file of fs9.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"))) {
3560
- const filePath = path8.join(cursorRulesDir, file);
3711
+ const cursorRulesDir = path9.join(dir, ".cursor", "rules");
3712
+ if (fs10.existsSync(cursorRulesDir)) {
3713
+ for (const file of fs10.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"))) {
3714
+ const filePath = path9.join(cursorRulesDir, file);
3561
3715
  items.push({
3562
3716
  type: "rule",
3563
3717
  platform: "cursor",
@@ -3567,12 +3721,12 @@ function scanLocalState(dir) {
3567
3721
  });
3568
3722
  }
3569
3723
  }
3570
- const cursorSkillsDir = path8.join(dir, ".cursor", "skills");
3571
- if (fs9.existsSync(cursorSkillsDir)) {
3724
+ const cursorSkillsDir = path9.join(dir, ".cursor", "skills");
3725
+ if (fs10.existsSync(cursorSkillsDir)) {
3572
3726
  try {
3573
- for (const name of fs9.readdirSync(cursorSkillsDir)) {
3574
- const skillFile = path8.join(cursorSkillsDir, name, "SKILL.md");
3575
- if (fs9.existsSync(skillFile)) {
3727
+ for (const name of fs10.readdirSync(cursorSkillsDir)) {
3728
+ const skillFile = path9.join(cursorSkillsDir, name, "SKILL.md");
3729
+ if (fs10.existsSync(skillFile)) {
3576
3730
  items.push({
3577
3731
  type: "skill",
3578
3732
  platform: "cursor",
@@ -3586,10 +3740,10 @@ function scanLocalState(dir) {
3586
3740
  warnScanSkip(".cursor/skills", error);
3587
3741
  }
3588
3742
  }
3589
- const cursorMcpPath = path8.join(dir, ".cursor", "mcp.json");
3590
- if (fs9.existsSync(cursorMcpPath)) {
3743
+ const cursorMcpPath = path9.join(dir, ".cursor", "mcp.json");
3744
+ if (fs10.existsSync(cursorMcpPath)) {
3591
3745
  try {
3592
- const mcpJson = JSON.parse(fs9.readFileSync(cursorMcpPath, "utf-8"));
3746
+ const mcpJson = JSON.parse(fs10.readFileSync(cursorMcpPath, "utf-8"));
3593
3747
  if (mcpJson.mcpServers) {
3594
3748
  for (const name of Object.keys(mcpJson.mcpServers)) {
3595
3749
  items.push({
@@ -3608,7 +3762,7 @@ function scanLocalState(dir) {
3608
3762
  return items;
3609
3763
  }
3610
3764
  function hashFile(filePath) {
3611
- const text = fs9.readFileSync(filePath, "utf-8");
3765
+ const text = fs10.readFileSync(filePath, "utf-8");
3612
3766
  return crypto2.createHash("sha256").update(JSON.stringify({ text })).digest("hex");
3613
3767
  }
3614
3768
  function hashJson(obj) {
@@ -3621,37 +3775,37 @@ function warnScanSkip(target, error) {
3621
3775
  function getCursorConfigDir() {
3622
3776
  const home = os4.homedir();
3623
3777
  if (process.platform === "darwin") {
3624
- return path8.join(home, "Library", "Application Support", "Cursor");
3778
+ return path9.join(home, "Library", "Application Support", "Cursor");
3625
3779
  }
3626
3780
  if (process.platform === "win32") {
3627
- return path8.join(home, "AppData", "Roaming", "Cursor");
3781
+ return path9.join(home, "AppData", "Roaming", "Cursor");
3628
3782
  }
3629
- return path8.join(home, ".config", "Cursor");
3783
+ return path9.join(home, ".config", "Cursor");
3630
3784
  }
3631
3785
 
3632
3786
  // src/lib/hooks.ts
3633
3787
  init_resolve_caliber();
3634
- import fs10 from "fs";
3635
- import path9 from "path";
3788
+ import fs11 from "fs";
3789
+ import path10 from "path";
3636
3790
  import { execSync as execSync8 } from "child_process";
3637
- var SETTINGS_PATH = path9.join(".claude", "settings.json");
3791
+ var SETTINGS_PATH = path10.join(".claude", "settings.json");
3638
3792
  var REFRESH_TAIL = "refresh --quiet";
3639
3793
  var HOOK_DESCRIPTION = "Caliber: auto-refreshing docs based on code changes";
3640
3794
  function getHookCommand() {
3641
3795
  return `${resolveCaliber()} ${REFRESH_TAIL}`;
3642
3796
  }
3643
3797
  function readSettings() {
3644
- if (!fs10.existsSync(SETTINGS_PATH)) return {};
3798
+ if (!fs11.existsSync(SETTINGS_PATH)) return {};
3645
3799
  try {
3646
- return JSON.parse(fs10.readFileSync(SETTINGS_PATH, "utf-8"));
3800
+ return JSON.parse(fs11.readFileSync(SETTINGS_PATH, "utf-8"));
3647
3801
  } catch {
3648
3802
  return {};
3649
3803
  }
3650
3804
  }
3651
3805
  function writeSettings(settings) {
3652
- const dir = path9.dirname(SETTINGS_PATH);
3653
- if (!fs10.existsSync(dir)) fs10.mkdirSync(dir, { recursive: true });
3654
- fs10.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
3806
+ const dir = path10.dirname(SETTINGS_PATH);
3807
+ if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
3808
+ fs11.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
3655
3809
  }
3656
3810
  function findHookIndex(sessionEnd) {
3657
3811
  return sessionEnd.findIndex(
@@ -3717,19 +3871,19 @@ ${PRECOMMIT_END}`;
3717
3871
  function getGitHooksDir() {
3718
3872
  try {
3719
3873
  const gitDir = execSync8("git rev-parse --git-dir", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3720
- return path9.join(gitDir, "hooks");
3874
+ return path10.join(gitDir, "hooks");
3721
3875
  } catch {
3722
3876
  return null;
3723
3877
  }
3724
3878
  }
3725
3879
  function getPreCommitPath() {
3726
3880
  const hooksDir = getGitHooksDir();
3727
- return hooksDir ? path9.join(hooksDir, "pre-commit") : null;
3881
+ return hooksDir ? path10.join(hooksDir, "pre-commit") : null;
3728
3882
  }
3729
3883
  function isPreCommitHookInstalled() {
3730
3884
  const hookPath = getPreCommitPath();
3731
- if (!hookPath || !fs10.existsSync(hookPath)) return false;
3732
- const content = fs10.readFileSync(hookPath, "utf-8");
3885
+ if (!hookPath || !fs11.existsSync(hookPath)) return false;
3886
+ const content = fs11.readFileSync(hookPath, "utf-8");
3733
3887
  return content.includes(PRECOMMIT_START);
3734
3888
  }
3735
3889
  function installPreCommitHook() {
@@ -3738,42 +3892,42 @@ function installPreCommitHook() {
3738
3892
  }
3739
3893
  const hookPath = getPreCommitPath();
3740
3894
  if (!hookPath) return { installed: false, alreadyInstalled: false };
3741
- const hooksDir = path9.dirname(hookPath);
3742
- if (!fs10.existsSync(hooksDir)) fs10.mkdirSync(hooksDir, { recursive: true });
3895
+ const hooksDir = path10.dirname(hookPath);
3896
+ if (!fs11.existsSync(hooksDir)) fs11.mkdirSync(hooksDir, { recursive: true });
3743
3897
  let content = "";
3744
- if (fs10.existsSync(hookPath)) {
3745
- content = fs10.readFileSync(hookPath, "utf-8");
3898
+ if (fs11.existsSync(hookPath)) {
3899
+ content = fs11.readFileSync(hookPath, "utf-8");
3746
3900
  if (!content.endsWith("\n")) content += "\n";
3747
3901
  content += "\n" + getPrecommitBlock() + "\n";
3748
3902
  } else {
3749
3903
  content = "#!/bin/sh\n\n" + getPrecommitBlock() + "\n";
3750
3904
  }
3751
- fs10.writeFileSync(hookPath, content);
3752
- fs10.chmodSync(hookPath, 493);
3905
+ fs11.writeFileSync(hookPath, content);
3906
+ fs11.chmodSync(hookPath, 493);
3753
3907
  return { installed: true, alreadyInstalled: false };
3754
3908
  }
3755
3909
  function removePreCommitHook() {
3756
3910
  const hookPath = getPreCommitPath();
3757
- if (!hookPath || !fs10.existsSync(hookPath)) {
3911
+ if (!hookPath || !fs11.existsSync(hookPath)) {
3758
3912
  return { removed: false, notFound: true };
3759
3913
  }
3760
- let content = fs10.readFileSync(hookPath, "utf-8");
3914
+ let content = fs11.readFileSync(hookPath, "utf-8");
3761
3915
  if (!content.includes(PRECOMMIT_START)) {
3762
3916
  return { removed: false, notFound: true };
3763
3917
  }
3764
3918
  const regex = new RegExp(`\\n?${PRECOMMIT_START}[\\s\\S]*?${PRECOMMIT_END}\\n?`);
3765
3919
  content = content.replace(regex, "\n");
3766
3920
  if (content.trim() === "#!/bin/sh" || content.trim() === "") {
3767
- fs10.unlinkSync(hookPath);
3921
+ fs11.unlinkSync(hookPath);
3768
3922
  } else {
3769
- fs10.writeFileSync(hookPath, content);
3923
+ fs11.writeFileSync(hookPath, content);
3770
3924
  }
3771
3925
  return { removed: true, notFound: false };
3772
3926
  }
3773
3927
 
3774
3928
  // src/fingerprint/sources.ts
3775
- import fs11 from "fs";
3776
- import path10 from "path";
3929
+ import fs12 from "fs";
3930
+ import path11 from "path";
3777
3931
 
3778
3932
  // src/scoring/utils.ts
3779
3933
  import { existsSync as existsSync2, readFileSync, readdirSync } from "fs";
@@ -4092,7 +4246,7 @@ var SOURCE_CONTENT_LIMIT = 2e3;
4092
4246
  var README_CONTENT_LIMIT = 1e3;
4093
4247
  var ORIGIN_PRIORITY = { cli: 0, config: 1, workspace: 2 };
4094
4248
  function loadSourcesConfig(dir) {
4095
- const configPath = path10.join(dir, ".caliber", "sources.json");
4249
+ const configPath = path11.join(dir, ".caliber", "sources.json");
4096
4250
  const content = readFileOrNull(configPath);
4097
4251
  if (!content) return [];
4098
4252
  try {
@@ -4110,29 +4264,29 @@ function loadSourcesConfig(dir) {
4110
4264
  }
4111
4265
  }
4112
4266
  function writeSourcesConfig(dir, sources2) {
4113
- const configDir = path10.join(dir, ".caliber");
4114
- if (!fs11.existsSync(configDir)) {
4115
- fs11.mkdirSync(configDir, { recursive: true });
4267
+ const configDir = path11.join(dir, ".caliber");
4268
+ if (!fs12.existsSync(configDir)) {
4269
+ fs12.mkdirSync(configDir, { recursive: true });
4116
4270
  }
4117
- const configPath = path10.join(configDir, "sources.json");
4118
- fs11.writeFileSync(configPath, JSON.stringify({ sources: sources2 }, null, 2) + "\n", "utf-8");
4271
+ const configPath = path11.join(configDir, "sources.json");
4272
+ fs12.writeFileSync(configPath, JSON.stringify({ sources: sources2 }, null, 2) + "\n", "utf-8");
4119
4273
  }
4120
4274
  function detectSourceType(absPath) {
4121
4275
  try {
4122
- return fs11.statSync(absPath).isDirectory() ? "repo" : "file";
4276
+ return fs12.statSync(absPath).isDirectory() ? "repo" : "file";
4123
4277
  } catch {
4124
4278
  return "file";
4125
4279
  }
4126
4280
  }
4127
4281
  function isInsideDir(childPath, parentDir) {
4128
- const relative2 = path10.relative(parentDir, childPath);
4129
- return !relative2.startsWith("..") && !path10.isAbsolute(relative2);
4282
+ const relative2 = path11.relative(parentDir, childPath);
4283
+ return !relative2.startsWith("..") && !path11.isAbsolute(relative2);
4130
4284
  }
4131
4285
  function resolveAllSources(dir, cliSources, workspaces) {
4132
4286
  const seen = /* @__PURE__ */ new Map();
4133
- const projectRoot = path10.resolve(dir);
4287
+ const projectRoot = path11.resolve(dir);
4134
4288
  for (const src of cliSources) {
4135
- const absPath = path10.resolve(dir, src);
4289
+ const absPath = path11.resolve(dir, src);
4136
4290
  if (seen.has(absPath)) continue;
4137
4291
  const type = detectSourceType(absPath);
4138
4292
  seen.set(absPath, {
@@ -4145,12 +4299,12 @@ function resolveAllSources(dir, cliSources, workspaces) {
4145
4299
  for (const cfg of configSources) {
4146
4300
  if (cfg.type === "url") continue;
4147
4301
  if (!cfg.path) continue;
4148
- const absPath = path10.resolve(dir, cfg.path);
4302
+ const absPath = path11.resolve(dir, cfg.path);
4149
4303
  if (seen.has(absPath)) continue;
4150
4304
  seen.set(absPath, { absPath, config: cfg, origin: "config" });
4151
4305
  }
4152
4306
  for (const ws of workspaces) {
4153
- const absPath = path10.resolve(dir, ws);
4307
+ const absPath = path11.resolve(dir, ws);
4154
4308
  if (seen.has(absPath)) continue;
4155
4309
  if (!isInsideDir(absPath, projectRoot)) continue;
4156
4310
  seen.set(absPath, {
@@ -4163,7 +4317,7 @@ function resolveAllSources(dir, cliSources, workspaces) {
4163
4317
  for (const [absPath, resolved] of seen) {
4164
4318
  let stat;
4165
4319
  try {
4166
- stat = fs11.statSync(absPath);
4320
+ stat = fs12.statSync(absPath);
4167
4321
  } catch {
4168
4322
  console.warn(`Source ${resolved.config.path || absPath} not found, skipping`);
4169
4323
  continue;
@@ -4192,13 +4346,13 @@ function collectSourceSummary(resolved, projectDir) {
4192
4346
  if (config.type === "file") {
4193
4347
  return collectFileSummary(resolved, projectDir);
4194
4348
  }
4195
- const summaryPath = path10.join(absPath, ".caliber", "summary.json");
4349
+ const summaryPath = path11.join(absPath, ".caliber", "summary.json");
4196
4350
  const summaryContent = readFileOrNull(summaryPath);
4197
4351
  if (summaryContent) {
4198
4352
  try {
4199
4353
  const published = JSON.parse(summaryContent);
4200
4354
  return {
4201
- name: published.name || path10.basename(absPath),
4355
+ name: published.name || path11.basename(absPath),
4202
4356
  type: "repo",
4203
4357
  role: config.role || published.role || "related-repo",
4204
4358
  description: config.description || published.description || "",
@@ -4218,18 +4372,18 @@ function collectRepoSummary(resolved, projectDir) {
4218
4372
  let topLevelDirs;
4219
4373
  let keyFiles;
4220
4374
  try {
4221
- const entries = fs11.readdirSync(absPath, { withFileTypes: true });
4375
+ const entries = fs12.readdirSync(absPath, { withFileTypes: true });
4222
4376
  topLevelDirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name).slice(0, 20);
4223
4377
  keyFiles = entries.filter((e) => e.isFile() && !e.name.startsWith(".")).map((e) => e.name).slice(0, 15);
4224
4378
  } catch {
4225
4379
  }
4226
- const claudeMdContent = readFileOrNull(path10.join(absPath, "CLAUDE.md"));
4380
+ const claudeMdContent = readFileOrNull(path11.join(absPath, "CLAUDE.md"));
4227
4381
  const existingClaudeMd = claudeMdContent ? claudeMdContent.slice(0, SOURCE_CONTENT_LIMIT) : void 0;
4228
- const readmeContent = readFileOrNull(path10.join(absPath, "README.md"));
4382
+ const readmeContent = readFileOrNull(path11.join(absPath, "README.md"));
4229
4383
  const readmeExcerpt = readmeContent ? readmeContent.slice(0, README_CONTENT_LIMIT) : void 0;
4230
4384
  const gitRemoteUrl = getGitRemoteUrl(absPath);
4231
4385
  return {
4232
- name: packageName || path10.basename(absPath),
4386
+ name: packageName || path11.basename(absPath),
4233
4387
  type: "repo",
4234
4388
  role: config.role || "related-repo",
4235
4389
  description: config.description || "",
@@ -4246,7 +4400,7 @@ function collectFileSummary(resolved, projectDir) {
4246
4400
  const { config, origin, absPath } = resolved;
4247
4401
  const content = readFileOrNull(absPath);
4248
4402
  return {
4249
- name: path10.basename(absPath),
4403
+ name: path11.basename(absPath),
4250
4404
  type: "file",
4251
4405
  role: config.role || "reference-doc",
4252
4406
  description: config.description || content?.slice(0, 100).split("\n")[0] || "",
@@ -4889,17 +5043,17 @@ import fs19 from "fs";
4889
5043
 
4890
5044
  // src/writers/claude/index.ts
4891
5045
  init_pre_commit_block();
4892
- import fs12 from "fs";
4893
- import path11 from "path";
5046
+ import fs13 from "fs";
5047
+ import path12 from "path";
4894
5048
  function writeClaudeConfig(config) {
4895
5049
  const written = [];
4896
- fs12.writeFileSync("CLAUDE.md", appendSyncBlock(appendLearningsBlock(appendPreCommitBlock(config.claudeMd))));
5050
+ fs13.writeFileSync("CLAUDE.md", appendManagedBlocks(config.claudeMd, "claude"));
4897
5051
  written.push("CLAUDE.md");
4898
5052
  if (config.skills?.length) {
4899
5053
  for (const skill of config.skills) {
4900
- const skillDir = path11.join(".claude", "skills", skill.name);
4901
- if (!fs12.existsSync(skillDir)) fs12.mkdirSync(skillDir, { recursive: true });
4902
- const skillPath = path11.join(skillDir, "SKILL.md");
5054
+ const skillDir = path12.join(".claude", "skills", skill.name);
5055
+ if (!fs13.existsSync(skillDir)) fs13.mkdirSync(skillDir, { recursive: true });
5056
+ const skillPath = path12.join(skillDir, "SKILL.md");
4903
5057
  const frontmatter = [
4904
5058
  "---",
4905
5059
  `name: ${skill.name}`,
@@ -4907,21 +5061,21 @@ function writeClaudeConfig(config) {
4907
5061
  "---",
4908
5062
  ""
4909
5063
  ].join("\n");
4910
- fs12.writeFileSync(skillPath, frontmatter + skill.content);
5064
+ fs13.writeFileSync(skillPath, frontmatter + skill.content);
4911
5065
  written.push(skillPath);
4912
5066
  }
4913
5067
  }
4914
5068
  if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
4915
5069
  let existingServers = {};
4916
5070
  try {
4917
- if (fs12.existsSync(".mcp.json")) {
4918
- const existing = JSON.parse(fs12.readFileSync(".mcp.json", "utf-8"));
5071
+ if (fs13.existsSync(".mcp.json")) {
5072
+ const existing = JSON.parse(fs13.readFileSync(".mcp.json", "utf-8"));
4919
5073
  if (existing.mcpServers) existingServers = existing.mcpServers;
4920
5074
  }
4921
5075
  } catch {
4922
5076
  }
4923
5077
  const mergedServers = { ...existingServers, ...config.mcpServers };
4924
- fs12.writeFileSync(".mcp.json", JSON.stringify({ mcpServers: mergedServers }, null, 2));
5078
+ fs13.writeFileSync(".mcp.json", JSON.stringify({ mcpServers: mergedServers }, null, 2));
4925
5079
  written.push(".mcp.json");
4926
5080
  }
4927
5081
  return written;
@@ -4929,30 +5083,31 @@ function writeClaudeConfig(config) {
4929
5083
 
4930
5084
  // src/writers/cursor/index.ts
4931
5085
  init_pre_commit_block();
4932
- import fs13 from "fs";
4933
- import path12 from "path";
5086
+ import fs14 from "fs";
5087
+ import path13 from "path";
4934
5088
  function writeCursorConfig(config) {
4935
5089
  const written = [];
4936
5090
  if (config.cursorrules) {
4937
- fs13.writeFileSync(".cursorrules", config.cursorrules);
5091
+ fs14.writeFileSync(".cursorrules", config.cursorrules);
4938
5092
  written.push(".cursorrules");
4939
5093
  }
4940
5094
  const preCommitRule = getCursorPreCommitRule();
4941
5095
  const learningsRule = getCursorLearningsRule();
4942
5096
  const syncRule = getCursorSyncRule();
4943
- const allRules = [...config.rules || [], preCommitRule, learningsRule, syncRule];
4944
- const rulesDir = path12.join(".cursor", "rules");
4945
- if (!fs13.existsSync(rulesDir)) fs13.mkdirSync(rulesDir, { recursive: true });
5097
+ const setupRule = getCursorSetupRule();
5098
+ const allRules = [...config.rules || [], preCommitRule, learningsRule, syncRule, setupRule];
5099
+ const rulesDir = path13.join(".cursor", "rules");
5100
+ if (!fs14.existsSync(rulesDir)) fs14.mkdirSync(rulesDir, { recursive: true });
4946
5101
  for (const rule of allRules) {
4947
- const rulePath = path12.join(rulesDir, rule.filename);
4948
- fs13.writeFileSync(rulePath, rule.content);
5102
+ const rulePath = path13.join(rulesDir, rule.filename);
5103
+ fs14.writeFileSync(rulePath, rule.content);
4949
5104
  written.push(rulePath);
4950
5105
  }
4951
5106
  if (config.skills?.length) {
4952
5107
  for (const skill of config.skills) {
4953
- const skillDir = path12.join(".cursor", "skills", skill.name);
4954
- if (!fs13.existsSync(skillDir)) fs13.mkdirSync(skillDir, { recursive: true });
4955
- const skillPath = path12.join(skillDir, "SKILL.md");
5108
+ const skillDir = path13.join(".cursor", "skills", skill.name);
5109
+ if (!fs14.existsSync(skillDir)) fs14.mkdirSync(skillDir, { recursive: true });
5110
+ const skillPath = path13.join(skillDir, "SKILL.md");
4956
5111
  const frontmatter = [
4957
5112
  "---",
4958
5113
  `name: ${skill.name}`,
@@ -4960,24 +5115,24 @@ function writeCursorConfig(config) {
4960
5115
  "---",
4961
5116
  ""
4962
5117
  ].join("\n");
4963
- fs13.writeFileSync(skillPath, frontmatter + skill.content);
5118
+ fs14.writeFileSync(skillPath, frontmatter + skill.content);
4964
5119
  written.push(skillPath);
4965
5120
  }
4966
5121
  }
4967
5122
  if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
4968
5123
  const cursorDir = ".cursor";
4969
- if (!fs13.existsSync(cursorDir)) fs13.mkdirSync(cursorDir, { recursive: true });
4970
- const mcpPath = path12.join(cursorDir, "mcp.json");
5124
+ if (!fs14.existsSync(cursorDir)) fs14.mkdirSync(cursorDir, { recursive: true });
5125
+ const mcpPath = path13.join(cursorDir, "mcp.json");
4971
5126
  let existingServers = {};
4972
5127
  try {
4973
- if (fs13.existsSync(mcpPath)) {
4974
- const existing = JSON.parse(fs13.readFileSync(mcpPath, "utf-8"));
5128
+ if (fs14.existsSync(mcpPath)) {
5129
+ const existing = JSON.parse(fs14.readFileSync(mcpPath, "utf-8"));
4975
5130
  if (existing.mcpServers) existingServers = existing.mcpServers;
4976
5131
  }
4977
5132
  } catch {
4978
5133
  }
4979
5134
  const mergedServers = { ...existingServers, ...config.mcpServers };
4980
- fs13.writeFileSync(mcpPath, JSON.stringify({ mcpServers: mergedServers }, null, 2));
5135
+ fs14.writeFileSync(mcpPath, JSON.stringify({ mcpServers: mergedServers }, null, 2));
4981
5136
  written.push(mcpPath);
4982
5137
  }
4983
5138
  return written;
@@ -4985,17 +5140,17 @@ function writeCursorConfig(config) {
4985
5140
 
4986
5141
  // src/writers/codex/index.ts
4987
5142
  init_pre_commit_block();
4988
- import fs14 from "fs";
4989
- import path13 from "path";
5143
+ import fs15 from "fs";
5144
+ import path14 from "path";
4990
5145
  function writeCodexConfig(config) {
4991
5146
  const written = [];
4992
- fs14.writeFileSync("AGENTS.md", appendLearningsBlock(appendPreCommitBlock(config.agentsMd)));
5147
+ fs15.writeFileSync("AGENTS.md", appendManagedBlocks(config.agentsMd, "codex"));
4993
5148
  written.push("AGENTS.md");
4994
5149
  if (config.skills?.length) {
4995
5150
  for (const skill of config.skills) {
4996
- const skillDir = path13.join(".agents", "skills", skill.name);
4997
- if (!fs14.existsSync(skillDir)) fs14.mkdirSync(skillDir, { recursive: true });
4998
- const skillPath = path13.join(skillDir, "SKILL.md");
5151
+ const skillDir = path14.join(".agents", "skills", skill.name);
5152
+ if (!fs15.existsSync(skillDir)) fs15.mkdirSync(skillDir, { recursive: true });
5153
+ const skillPath = path14.join(skillDir, "SKILL.md");
4999
5154
  const frontmatter = [
5000
5155
  "---",
5001
5156
  `name: ${skill.name}`,
@@ -5003,7 +5158,7 @@ function writeCodexConfig(config) {
5003
5158
  "---",
5004
5159
  ""
5005
5160
  ].join("\n");
5006
- fs14.writeFileSync(skillPath, frontmatter + skill.content);
5161
+ fs15.writeFileSync(skillPath, frontmatter + skill.content);
5007
5162
  written.push(skillPath);
5008
5163
  }
5009
5164
  }
@@ -5012,20 +5167,20 @@ function writeCodexConfig(config) {
5012
5167
 
5013
5168
  // src/writers/github-copilot/index.ts
5014
5169
  init_pre_commit_block();
5015
- import fs15 from "fs";
5016
- import path14 from "path";
5170
+ import fs16 from "fs";
5171
+ import path15 from "path";
5017
5172
  function writeGithubCopilotConfig(config) {
5018
5173
  const written = [];
5019
5174
  if (config.instructions) {
5020
- fs15.mkdirSync(".github", { recursive: true });
5021
- fs15.writeFileSync(path14.join(".github", "copilot-instructions.md"), appendSyncBlock(appendLearningsBlock(appendPreCommitBlock(config.instructions))));
5175
+ fs16.mkdirSync(".github", { recursive: true });
5176
+ fs16.writeFileSync(path15.join(".github", "copilot-instructions.md"), appendManagedBlocks(config.instructions, "copilot"));
5022
5177
  written.push(".github/copilot-instructions.md");
5023
5178
  }
5024
5179
  if (config.instructionFiles?.length) {
5025
- const instructionsDir = path14.join(".github", "instructions");
5026
- fs15.mkdirSync(instructionsDir, { recursive: true });
5180
+ const instructionsDir = path15.join(".github", "instructions");
5181
+ fs16.mkdirSync(instructionsDir, { recursive: true });
5027
5182
  for (const file of config.instructionFiles) {
5028
- fs15.writeFileSync(path14.join(instructionsDir, file.filename), file.content);
5183
+ fs16.writeFileSync(path15.join(instructionsDir, file.filename), file.content);
5029
5184
  written.push(`.github/instructions/${file.filename}`);
5030
5185
  }
5031
5186
  }
@@ -5033,30 +5188,30 @@ function writeGithubCopilotConfig(config) {
5033
5188
  }
5034
5189
 
5035
5190
  // src/writers/backup.ts
5036
- import fs16 from "fs";
5037
- import path15 from "path";
5191
+ import fs17 from "fs";
5192
+ import path16 from "path";
5038
5193
  function createBackup(files) {
5039
5194
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5040
- const backupDir = path15.join(BACKUPS_DIR, timestamp);
5195
+ const backupDir = path16.join(BACKUPS_DIR, timestamp);
5041
5196
  for (const file of files) {
5042
- if (!fs16.existsSync(file)) continue;
5043
- const dest = path15.join(backupDir, file);
5044
- const destDir = path15.dirname(dest);
5045
- if (!fs16.existsSync(destDir)) {
5046
- fs16.mkdirSync(destDir, { recursive: true });
5197
+ if (!fs17.existsSync(file)) continue;
5198
+ const dest = path16.join(backupDir, file);
5199
+ const destDir = path16.dirname(dest);
5200
+ if (!fs17.existsSync(destDir)) {
5201
+ fs17.mkdirSync(destDir, { recursive: true });
5047
5202
  }
5048
- fs16.copyFileSync(file, dest);
5203
+ fs17.copyFileSync(file, dest);
5049
5204
  }
5050
5205
  return backupDir;
5051
5206
  }
5052
5207
  function restoreBackup(backupDir, file) {
5053
- const backupFile = path15.join(backupDir, file);
5054
- if (!fs16.existsSync(backupFile)) return false;
5055
- const destDir = path15.dirname(file);
5056
- if (!fs16.existsSync(destDir)) {
5057
- fs16.mkdirSync(destDir, { recursive: true });
5208
+ const backupFile = path16.join(backupDir, file);
5209
+ if (!fs17.existsSync(backupFile)) return false;
5210
+ const destDir = path16.dirname(file);
5211
+ if (!fs17.existsSync(destDir)) {
5212
+ fs17.mkdirSync(destDir, { recursive: true });
5058
5213
  }
5059
- fs16.copyFileSync(backupFile, file);
5214
+ fs17.copyFileSync(backupFile, file);
5060
5215
  return true;
5061
5216
  }
5062
5217
 
@@ -9234,8 +9389,9 @@ async function initCommand(options) {
9234
9389
  if (options.agent) {
9235
9390
  targetAgent = options.agent;
9236
9391
  } else if (options.autoApprove) {
9237
- targetAgent = ["claude"];
9238
- log(options.verbose, "Auto-approve: defaulting to claude agent");
9392
+ const detected = detectAgents(process.cwd());
9393
+ targetAgent = detected.length > 0 ? detected : ["claude"];
9394
+ log(options.verbose, `Auto-approve: using ${targetAgent.join(", ")}`);
9239
9395
  } else {
9240
9396
  const detected = detectAgents(process.cwd());
9241
9397
  targetAgent = await promptAgent(detected.length > 0 ? detected : void 0);
@@ -9316,12 +9472,11 @@ async function initCommand(options) {
9316
9472
  }
9317
9473
  if (skipGeneration) {
9318
9474
  const {
9319
- appendPreCommitBlock: appendPreCommitBlock2,
9320
- appendLearningsBlock: appendLearningsBlock2,
9321
- appendSyncBlock: appendSyncBlock2,
9475
+ appendManagedBlocks: appendManagedBlocks2,
9322
9476
  getCursorPreCommitRule: getCursorPreCommitRule2,
9323
9477
  getCursorLearningsRule: getCursorLearningsRule2,
9324
- getCursorSyncRule: getCursorSyncRule2
9478
+ getCursorSyncRule: getCursorSyncRule2,
9479
+ getCursorSetupRule: getCursorSetupRule2
9325
9480
  } = await Promise.resolve().then(() => (init_pre_commit_block(), pre_commit_block_exports));
9326
9481
  const claudeMdPath = "CLAUDE.md";
9327
9482
  let claudeContent = "";
@@ -9333,7 +9488,7 @@ async function initCommand(options) {
9333
9488
  claudeContent = `# ${path26.basename(process.cwd())}
9334
9489
  `;
9335
9490
  }
9336
- const updatedClaude = appendSyncBlock2(appendLearningsBlock2(appendPreCommitBlock2(claudeContent)));
9491
+ const updatedClaude = appendManagedBlocks2(claudeContent, "claude");
9337
9492
  if (updatedClaude !== claudeContent || !fs33.existsSync(claudeMdPath)) {
9338
9493
  fs33.writeFileSync(claudeMdPath, updatedClaude);
9339
9494
  console.log(` ${chalk14.green("\u2713")} CLAUDE.md \u2014 added Caliber sync instructions`);
@@ -9341,7 +9496,7 @@ async function initCommand(options) {
9341
9496
  if (targetAgent.includes("cursor")) {
9342
9497
  const rulesDir = path26.join(".cursor", "rules");
9343
9498
  if (!fs33.existsSync(rulesDir)) fs33.mkdirSync(rulesDir, { recursive: true });
9344
- for (const rule of [getCursorPreCommitRule2(), getCursorLearningsRule2(), getCursorSyncRule2()]) {
9499
+ for (const rule of [getCursorPreCommitRule2(), getCursorLearningsRule2(), getCursorSyncRule2(), getCursorSetupRule2()]) {
9345
9500
  fs33.writeFileSync(path26.join(rulesDir, rule.filename), rule.content);
9346
9501
  }
9347
9502
  console.log(` ${chalk14.green("\u2713")} Cursor rules \u2014 added Caliber sync rules`);
@@ -9358,7 +9513,7 @@ async function initCommand(options) {
9358
9513
  copilotContent = `# ${path26.basename(process.cwd())}
9359
9514
  `;
9360
9515
  }
9361
- const updatedCopilot = appendSyncBlock2(appendLearningsBlock2(appendPreCommitBlock2(copilotContent)));
9516
+ const updatedCopilot = appendManagedBlocks2(copilotContent, "copilot");
9362
9517
  if (updatedCopilot !== copilotContent) {
9363
9518
  fs33.writeFileSync(copilotPath, updatedCopilot);
9364
9519
  console.log(` ${chalk14.green("\u2713")} Copilot instructions \u2014 added Caliber sync instructions`);
@@ -10137,14 +10292,24 @@ var MAX_DIFF_BYTES = 1e5;
10137
10292
  var DOC_PATTERNS = [
10138
10293
  "CLAUDE.md",
10139
10294
  "README.md",
10295
+ "AGENTS.md",
10140
10296
  ".cursorrules",
10141
10297
  ".cursor/rules/",
10298
+ ".cursor/skills/",
10142
10299
  ".claude/skills/",
10300
+ ".agents/skills/",
10301
+ ".github/copilot-instructions.md",
10302
+ ".github/instructions/",
10143
10303
  "CALIBER_LEARNINGS.md"
10144
10304
  ];
10145
10305
  function excludeArgs() {
10146
10306
  return DOC_PATTERNS.flatMap((p) => ["--", `:!${p}`]);
10147
10307
  }
10308
+ function truncateAtLine(text, maxBytes) {
10309
+ if (text.length <= maxBytes) return text;
10310
+ const cut = text.lastIndexOf("\n", maxBytes);
10311
+ return cut > 0 ? text.slice(0, cut) : text.slice(0, maxBytes);
10312
+ }
10148
10313
  function safeExec(cmd) {
10149
10314
  try {
10150
10315
  return execSync15(cmd, {
@@ -10190,9 +10355,9 @@ function collectDiff(lastSha) {
10190
10355
  const totalSize = committedDiff.length + stagedDiff.length + unstagedDiff.length;
10191
10356
  if (totalSize > MAX_DIFF_BYTES) {
10192
10357
  const ratio = MAX_DIFF_BYTES / totalSize;
10193
- committedDiff = committedDiff.slice(0, Math.floor(committedDiff.length * ratio));
10194
- stagedDiff = stagedDiff.slice(0, Math.floor(stagedDiff.length * ratio));
10195
- unstagedDiff = unstagedDiff.slice(0, Math.floor(unstagedDiff.length * ratio));
10358
+ committedDiff = truncateAtLine(committedDiff, Math.floor(committedDiff.length * ratio));
10359
+ stagedDiff = truncateAtLine(stagedDiff, Math.floor(stagedDiff.length * ratio));
10360
+ unstagedDiff = truncateAtLine(unstagedDiff, Math.floor(unstagedDiff.length * ratio));
10196
10361
  }
10197
10362
  const hasChanges = !!(committedDiff || stagedDiff || unstagedDiff || changedFiles.length);
10198
10363
  const parts = [];
@@ -10211,13 +10376,17 @@ import path28 from "path";
10211
10376
  function writeRefreshDocs(docs) {
10212
10377
  const written = [];
10213
10378
  if (docs.claudeMd) {
10214
- fs36.writeFileSync("CLAUDE.md", appendSyncBlock(appendLearningsBlock(appendPreCommitBlock(docs.claudeMd))));
10379
+ fs36.writeFileSync("CLAUDE.md", appendManagedBlocks(docs.claudeMd, "claude"));
10215
10380
  written.push("CLAUDE.md");
10216
10381
  }
10217
10382
  if (docs.readmeMd) {
10218
10383
  fs36.writeFileSync("README.md", docs.readmeMd);
10219
10384
  written.push("README.md");
10220
10385
  }
10386
+ if (docs.agentsMd) {
10387
+ fs36.writeFileSync("AGENTS.md", appendManagedBlocks(docs.agentsMd, "codex"));
10388
+ written.push("AGENTS.md");
10389
+ }
10221
10390
  if (docs.cursorrules) {
10222
10391
  fs36.writeFileSync(".cursorrules", docs.cursorrules);
10223
10392
  written.push(".cursorrules");
@@ -10240,7 +10409,7 @@ function writeRefreshDocs(docs) {
10240
10409
  }
10241
10410
  if (docs.copilotInstructions) {
10242
10411
  fs36.mkdirSync(".github", { recursive: true });
10243
- fs36.writeFileSync(path28.join(".github", "copilot-instructions.md"), appendSyncBlock(appendLearningsBlock(appendPreCommitBlock(docs.copilotInstructions))));
10412
+ fs36.writeFileSync(path28.join(".github", "copilot-instructions.md"), appendManagedBlocks(docs.copilotInstructions, "copilot"));
10244
10413
  written.push(".github/copilot-instructions.md");
10245
10414
  }
10246
10415
  if (docs.copilotInstructionFiles) {
@@ -10259,10 +10428,18 @@ init_config();
10259
10428
  async function refreshDocs(diff, existingDocs, projectContext, learnedSection, sources2) {
10260
10429
  const prompt = buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection, sources2);
10261
10430
  const fastModel = getFastModel();
10431
+ const docCount = [
10432
+ existingDocs.claudeMd,
10433
+ existingDocs.readmeMd,
10434
+ existingDocs.agentsMd,
10435
+ existingDocs.cursorrules,
10436
+ existingDocs.copilotInstructions
10437
+ ].filter(Boolean).length + (existingDocs.cursorRules?.length ?? 0) + (existingDocs.claudeSkills?.length ?? 0);
10438
+ const maxTokens = Math.min(32768, Math.max(8192, docCount * 4096));
10262
10439
  const raw = await llmCall({
10263
10440
  system: REFRESH_SYSTEM_PROMPT,
10264
10441
  prompt,
10265
- maxTokens: 16384,
10442
+ maxTokens,
10266
10443
  ...fastModel ? { model: fastModel } : {}
10267
10444
  });
10268
10445
  return parseJsonResponse(raw);
@@ -10321,6 +10498,21 @@ Changed files: ${diff.changedFiles.join(", ")}`);
10321
10498
  parts.push(rule.content);
10322
10499
  }
10323
10500
  }
10501
+ if (existingDocs.agentsMd) {
10502
+ parts.push("\n[AGENTS.md]");
10503
+ parts.push(existingDocs.agentsMd);
10504
+ }
10505
+ if (existingDocs.copilotInstructions) {
10506
+ parts.push("\n[.github/copilot-instructions.md]");
10507
+ parts.push(existingDocs.copilotInstructions);
10508
+ }
10509
+ if (existingDocs.copilotInstructionFiles?.length) {
10510
+ for (const file of existingDocs.copilotInstructionFiles) {
10511
+ parts.push(`
10512
+ [.github/instructions/${file.filename}]`);
10513
+ parts.push(file.content);
10514
+ }
10515
+ }
10324
10516
  if (learnedSection) {
10325
10517
  parts.push("\n--- Learned Patterns (from session learning) ---");
10326
10518
  parts.push("Consider these accumulated learnings when deciding what to update:");