@rely-ai/caliber 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +15 -5
  2. package/dist/bin.js +201 -131
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -43,8 +43,10 @@ If you already have these files, Caliber audits them against your actual codebas
43
43
  | `caliber recommend` | Discover skills from [skills.sh](https://skills.sh) |
44
44
  | `caliber undo` | Revert all changes made by Caliber |
45
45
  | `caliber status` | Show current setup status |
46
- | `caliber hooks install` | Install auto-refresh hook for Claude Code |
47
- | `caliber hooks remove` | Remove auto-refresh hook |
46
+ | `caliber hooks install` | Install Claude Code auto-refresh hook |
47
+ | `caliber hooks remove` | Remove Claude Code auto-refresh hook |
48
+ | `caliber hooks install-precommit` | Install git pre-commit hook for auto-refresh |
49
+ | `caliber hooks remove-precommit` | Remove git pre-commit hook |
48
50
  | `caliber hooks status` | Show installed hooks |
49
51
  | `caliber learn install` | Install session learning hooks |
50
52
  | `caliber learn status` | Show learned insights from sessions |
@@ -109,11 +111,19 @@ During `caliber init`, a before/after score is displayed so you can see the impr
109
111
 
110
112
  ### Auto-refresh
111
113
 
112
- After init, Caliber installs a Claude Code hook that automatically updates your docs when code changes:
114
+ During `caliber init`, you'll be prompted to choose how docs auto-refresh:
115
+
116
+ - **Claude Code hook** — refreshes docs when Claude Code sessions end
117
+ - **Git pre-commit hook** — refreshes docs before each commit
118
+ - **Both** — enables both hooks
119
+ - **Skip** — install later with `caliber hooks install` or `caliber hooks install-precommit`
113
120
 
114
121
  ```bash
115
- caliber hooks install # Install auto-refresh hook
116
- caliber hooks remove # Remove it
122
+ caliber hooks install # Install Claude Code hook
123
+ caliber hooks install-precommit # Install git pre-commit hook
124
+ caliber hooks remove # Remove Claude Code hook
125
+ caliber hooks remove-precommit # Remove pre-commit hook
126
+ caliber hooks status # Show installed hooks
117
127
  ```
118
128
 
119
129
  ### Session Learning
package/dist/bin.js CHANGED
@@ -95,98 +95,21 @@ function isGitRepo() {
95
95
  import fs from "fs";
96
96
  import path from "path";
97
97
  import { globSync } from "glob";
98
- var NODE_FRAMEWORK_DEPS = {
99
- react: "React",
100
- next: "Next.js",
101
- vue: "Vue",
102
- nuxt: "Nuxt",
103
- svelte: "Svelte",
104
- "@sveltejs/kit": "SvelteKit",
105
- angular: "Angular",
106
- "@angular/core": "Angular",
107
- express: "Express",
108
- fastify: "Fastify",
109
- hono: "Hono",
110
- nestjs: "NestJS",
111
- "@nestjs/core": "NestJS",
112
- tailwindcss: "Tailwind CSS",
113
- prisma: "Prisma",
114
- drizzle: "Drizzle",
115
- "drizzle-orm": "Drizzle",
116
- "@supabase/supabase-js": "Supabase",
117
- mongoose: "MongoDB",
118
- typeorm: "TypeORM",
119
- sequelize: "Sequelize",
120
- "better-auth": "Better Auth"
121
- };
122
- var PYTHON_FRAMEWORK_DEPS = {
123
- fastapi: "FastAPI",
124
- django: "Django",
125
- flask: "Flask",
126
- sqlalchemy: "SQLAlchemy",
127
- pydantic: "Pydantic",
128
- celery: "Celery",
129
- pytest: "pytest",
130
- uvicorn: "Uvicorn",
131
- starlette: "Starlette",
132
- httpx: "HTTPX",
133
- alembic: "Alembic",
134
- tortoise: "Tortoise ORM",
135
- "google-cloud-pubsub": "Google Pub/Sub",
136
- stripe: "Stripe",
137
- redis: "Redis"
138
- };
139
98
  var WORKSPACE_GLOBS = [
140
99
  "apps/*/package.json",
141
100
  "packages/*/package.json",
142
101
  "services/*/package.json",
143
102
  "libs/*/package.json"
144
103
  ];
145
- function detectNodeFrameworks(pkgPath) {
146
- try {
147
- const pkg3 = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
148
- const allDeps = { ...pkg3.dependencies, ...pkg3.devDependencies };
149
- const frameworks = [];
150
- for (const [dep, framework] of Object.entries(NODE_FRAMEWORK_DEPS)) {
151
- if (allDeps[dep]) frameworks.push(framework);
152
- }
153
- return frameworks;
154
- } catch {
155
- return [];
156
- }
157
- }
158
- function detectPythonFrameworks(dir) {
159
- const frameworks = [];
160
- const candidates = [
161
- path.join(dir, "pyproject.toml"),
162
- path.join(dir, "requirements.txt"),
163
- ...globSync("apps/*/pyproject.toml", { cwd: dir, absolute: true }),
164
- ...globSync("apps/*/requirements.txt", { cwd: dir, absolute: true }),
165
- ...globSync("services/*/pyproject.toml", { cwd: dir, absolute: true })
166
- ];
167
- for (const filePath of candidates) {
168
- if (!fs.existsSync(filePath)) continue;
169
- try {
170
- const content = fs.readFileSync(filePath, "utf-8").toLowerCase();
171
- for (const [dep, framework] of Object.entries(PYTHON_FRAMEWORK_DEPS)) {
172
- if (content.includes(dep)) frameworks.push(framework);
173
- }
174
- } catch {
175
- }
176
- }
177
- return frameworks;
178
- }
179
104
  function analyzePackageJson(dir) {
180
105
  const rootPkgPath = path.join(dir, "package.json");
181
106
  let name;
182
- const allFrameworks = [];
183
107
  const languages = [];
184
108
  if (fs.existsSync(rootPkgPath)) {
185
109
  try {
186
110
  const pkg3 = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
187
111
  name = pkg3.name;
188
112
  const allDeps = { ...pkg3.dependencies, ...pkg3.devDependencies };
189
- allFrameworks.push(...detectNodeFrameworks(rootPkgPath));
190
113
  if (allDeps.typescript || allDeps["@types/node"]) {
191
114
  languages.push("TypeScript");
192
115
  }
@@ -197,7 +120,6 @@ function analyzePackageJson(dir) {
197
120
  for (const glob of WORKSPACE_GLOBS) {
198
121
  const matches = globSync(glob, { cwd: dir, absolute: true });
199
122
  for (const pkgPath of matches) {
200
- allFrameworks.push(...detectNodeFrameworks(pkgPath));
201
123
  try {
202
124
  const pkg3 = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
203
125
  const deps = { ...pkg3.dependencies, ...pkg3.devDependencies };
@@ -208,10 +130,8 @@ function analyzePackageJson(dir) {
208
130
  }
209
131
  }
210
132
  }
211
- allFrameworks.push(...detectPythonFrameworks(dir));
212
133
  return {
213
134
  name,
214
- frameworks: [...new Set(allFrameworks)],
215
135
  languages: [...new Set(languages)]
216
136
  };
217
137
  }
@@ -1277,7 +1197,7 @@ function collectFingerprint(dir) {
1277
1197
  gitRemoteUrl,
1278
1198
  packageName: pkgInfo.name,
1279
1199
  languages,
1280
- frameworks: pkgInfo.frameworks,
1200
+ frameworks: [],
1281
1201
  fileTree,
1282
1202
  existingConfigs,
1283
1203
  codeAnalysis
@@ -1937,6 +1857,7 @@ import { createTwoFilesPatch } from "diff";
1937
1857
  // src/lib/hooks.ts
1938
1858
  import fs14 from "fs";
1939
1859
  import path13 from "path";
1860
+ import { execSync as execSync3 } from "child_process";
1940
1861
  var SETTINGS_PATH = path13.join(".claude", "settings.json");
1941
1862
  var HOOK_COMMAND = "caliber refresh --quiet";
1942
1863
  var HOOK_DESCRIPTION = "Caliber: auto-refreshing docs based on code changes";
@@ -1998,6 +1919,70 @@ function removeHook() {
1998
1919
  writeSettings(settings);
1999
1920
  return { removed: true, notFound: false };
2000
1921
  }
1922
+ var PRECOMMIT_START = "# caliber:pre-commit:start";
1923
+ var PRECOMMIT_END = "# caliber:pre-commit:end";
1924
+ var PRECOMMIT_BLOCK = `${PRECOMMIT_START}
1925
+ if command -v caliber >/dev/null 2>&1; then
1926
+ caliber refresh --quiet 2>/dev/null || true
1927
+ git diff --name-only -- CLAUDE.md .claude/ .cursor/ AGENTS.md 2>/dev/null | xargs git add 2>/dev/null || true
1928
+ fi
1929
+ ${PRECOMMIT_END}`;
1930
+ function getGitHooksDir() {
1931
+ try {
1932
+ const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1933
+ return path13.join(gitDir, "hooks");
1934
+ } catch {
1935
+ return null;
1936
+ }
1937
+ }
1938
+ function getPreCommitPath() {
1939
+ const hooksDir = getGitHooksDir();
1940
+ return hooksDir ? path13.join(hooksDir, "pre-commit") : null;
1941
+ }
1942
+ function isPreCommitHookInstalled() {
1943
+ const hookPath = getPreCommitPath();
1944
+ if (!hookPath || !fs14.existsSync(hookPath)) return false;
1945
+ const content = fs14.readFileSync(hookPath, "utf-8");
1946
+ return content.includes(PRECOMMIT_START);
1947
+ }
1948
+ function installPreCommitHook() {
1949
+ if (isPreCommitHookInstalled()) {
1950
+ return { installed: false, alreadyInstalled: true };
1951
+ }
1952
+ const hookPath = getPreCommitPath();
1953
+ if (!hookPath) return { installed: false, alreadyInstalled: false };
1954
+ const hooksDir = path13.dirname(hookPath);
1955
+ if (!fs14.existsSync(hooksDir)) fs14.mkdirSync(hooksDir, { recursive: true });
1956
+ let content = "";
1957
+ if (fs14.existsSync(hookPath)) {
1958
+ content = fs14.readFileSync(hookPath, "utf-8");
1959
+ if (!content.endsWith("\n")) content += "\n";
1960
+ content += "\n" + PRECOMMIT_BLOCK + "\n";
1961
+ } else {
1962
+ content = "#!/bin/sh\n\n" + PRECOMMIT_BLOCK + "\n";
1963
+ }
1964
+ fs14.writeFileSync(hookPath, content);
1965
+ fs14.chmodSync(hookPath, 493);
1966
+ return { installed: true, alreadyInstalled: false };
1967
+ }
1968
+ function removePreCommitHook() {
1969
+ const hookPath = getPreCommitPath();
1970
+ if (!hookPath || !fs14.existsSync(hookPath)) {
1971
+ return { removed: false, notFound: true };
1972
+ }
1973
+ let content = fs14.readFileSync(hookPath, "utf-8");
1974
+ if (!content.includes(PRECOMMIT_START)) {
1975
+ return { removed: false, notFound: true };
1976
+ }
1977
+ const regex = new RegExp(`\\n?${PRECOMMIT_START}[\\s\\S]*?${PRECOMMIT_END}\\n?`);
1978
+ content = content.replace(regex, "\n");
1979
+ if (content.trim() === "#!/bin/sh" || content.trim() === "") {
1980
+ fs14.unlinkSync(hookPath);
1981
+ } else {
1982
+ fs14.writeFileSync(hookPath, content);
1983
+ }
1984
+ return { removed: true, notFound: false };
1985
+ }
2001
1986
 
2002
1987
  // src/lib/learning-hooks.ts
2003
1988
  import fs15 from "fs";
@@ -2090,7 +2075,7 @@ function removeLearningHooks() {
2090
2075
  init_constants();
2091
2076
  import fs16 from "fs";
2092
2077
  import path15 from "path";
2093
- import { execSync as execSync3 } from "child_process";
2078
+ import { execSync as execSync4 } from "child_process";
2094
2079
  var STATE_FILE = path15.join(CALIBER_DIR, ".caliber-state.json");
2095
2080
  function readState() {
2096
2081
  try {
@@ -2108,7 +2093,7 @@ function writeState(state) {
2108
2093
  }
2109
2094
  function getCurrentHeadSha() {
2110
2095
  try {
2111
- return execSync3("git rev-parse HEAD", {
2096
+ return execSync4("git rev-parse HEAD", {
2112
2097
  encoding: "utf-8",
2113
2098
  stdio: ["pipe", "pipe", "pipe"]
2114
2099
  }).trim();
@@ -3334,17 +3319,26 @@ function displayScoreDelta(before, after) {
3334
3319
  const deltaColor = delta >= 0 ? chalk2.green : chalk2.red;
3335
3320
  const beforeGc = gradeColor(before.grade);
3336
3321
  const afterGc = gradeColor(after.grade);
3322
+ const BOX_INNER = 51;
3323
+ const scorePart = `Score: ${before.score} > ${after.score}`;
3324
+ const deltaPart = `${deltaStr} pts`;
3325
+ const gradePart = `${before.grade} > ${after.grade}`;
3326
+ const contentLen = 3 + scorePart.length + deltaPart.length + gradePart.length + 8;
3327
+ const totalPad = BOX_INNER - contentLen;
3328
+ const pad1 = Math.max(2, Math.ceil(totalPad / 2));
3329
+ const pad2 = Math.max(1, totalPad - pad1);
3330
+ const scoreLineFormatted = " Score: " + beforeGc(`${before.score}`) + chalk2.gray(" \u2192 ") + afterGc(`${after.score}`) + " ".repeat(pad1) + deltaColor(deltaPart) + " ".repeat(pad2) + beforeGc(before.grade) + chalk2.gray(" \u2192 ") + afterGc(after.grade);
3331
+ const visibleLen = 3 + scorePart.length + pad1 + deltaPart.length + pad2 + gradePart.length;
3332
+ const trailingPad = Math.max(0, BOX_INNER - visibleLen);
3333
+ const barWidth = Math.floor((BOX_INNER - 12) / 2);
3334
+ const barLine = ` ${progressBar(before.score, before.maxScore, barWidth)}` + chalk2.gray(" \u2192 ") + progressBar(after.score, after.maxScore, barWidth) + " ";
3337
3335
  console.log("");
3338
- console.log(chalk2.gray(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
3339
- console.log(chalk2.gray(" \u2502") + " " + chalk2.gray("\u2502"));
3340
- console.log(
3341
- chalk2.gray(" \u2502") + " Score: " + beforeGc(`${before.score}`) + chalk2.gray(" \u2192 ") + afterGc(`${after.score}`) + " " + deltaColor(`${deltaStr} pts`) + " " + beforeGc(before.grade) + chalk2.gray(" \u2192 ") + afterGc(after.grade) + " " + chalk2.gray("\u2502")
3342
- );
3343
- console.log(
3344
- chalk2.gray(" \u2502") + ` ${progressBar(before.score, before.maxScore, 18)}` + chalk2.gray(" \u2192 ") + `${progressBar(after.score, after.maxScore, 18)} ` + chalk2.gray("\u2502")
3345
- );
3346
- console.log(chalk2.gray(" \u2502") + " " + chalk2.gray("\u2502"));
3347
- console.log(chalk2.gray(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
3336
+ console.log(chalk2.gray(" \u256D" + "\u2500".repeat(BOX_INNER) + "\u256E"));
3337
+ console.log(chalk2.gray(" \u2502") + " ".repeat(BOX_INNER) + chalk2.gray("\u2502"));
3338
+ console.log(chalk2.gray(" \u2502") + scoreLineFormatted + " ".repeat(trailingPad) + chalk2.gray("\u2502"));
3339
+ console.log(chalk2.gray(" \u2502") + barLine + chalk2.gray("\u2502"));
3340
+ console.log(chalk2.gray(" \u2502") + " ".repeat(BOX_INNER) + chalk2.gray("\u2502"));
3341
+ console.log(chalk2.gray(" \u2570" + "\u2500".repeat(BOX_INNER) + "\u256F"));
3348
3342
  console.log("");
3349
3343
  const improved = after.checks.filter((ac) => {
3350
3344
  const bc = before.checks.find((b) => b.id === ac.id);
@@ -3380,10 +3374,10 @@ async function initCommand(options) {
3380
3374
  console.log(chalk3.dim(" against your actual codebase \u2014 keeping what works, fixing"));
3381
3375
  console.log(chalk3.dim(" what's stale, and adding what's missing.\n"));
3382
3376
  console.log(chalk3.bold(" How it works:\n"));
3383
- console.log(chalk3.dim(" 1. Scan Analyze your code, dependencies, and file structure"));
3384
- console.log(chalk3.dim(" 2. Generate AI creates config files tailored to your project"));
3385
- console.log(chalk3.dim(" 3. Review You accept, refine, or decline the generated setup"));
3386
- console.log(chalk3.dim(" 4. Apply Config files are written to your project\n"));
3377
+ console.log(chalk3.dim(" 1. Scan Analyze your code, dependencies, and existing configs"));
3378
+ console.log(chalk3.dim(" 2. Generate AI creates or improves config files for your project"));
3379
+ console.log(chalk3.dim(" 3. Review You accept, refine, or decline the proposed changes"));
3380
+ console.log(chalk3.dim(" 4. Apply Config files are written with backups\n"));
3387
3381
  console.log(chalk3.hex("#6366f1").bold(" Step 1/4 \u2014 Check LLM provider\n"));
3388
3382
  const config = loadConfig();
3389
3383
  if (!config) {
@@ -3398,13 +3392,12 @@ async function initCommand(options) {
3398
3392
  console.log(chalk3.dim(` Provider: ${config.provider} | Model: ${config.model}
3399
3393
  `));
3400
3394
  console.log(chalk3.hex("#6366f1").bold(" Step 2/4 \u2014 Scan project\n"));
3401
- console.log(chalk3.dim(" Detecting languages, frameworks, file structure, and existing configs.\n"));
3395
+ console.log(chalk3.dim(" Detecting languages, dependencies, file structure, and existing configs.\n"));
3402
3396
  const spinner = ora("Analyzing project...").start();
3403
3397
  const fingerprint = collectFingerprint(process.cwd());
3398
+ await enrichFingerprintWithLLM(fingerprint, process.cwd());
3404
3399
  spinner.succeed("Project analyzed");
3405
- const enrichmentPromise = enrichFingerprintWithLLM(fingerprint, process.cwd());
3406
3400
  console.log(chalk3.dim(` Languages: ${fingerprint.languages.join(", ") || "none detected"}`));
3407
- console.log(chalk3.dim(` Frameworks: ${fingerprint.frameworks.join(", ") || "none detected"}`));
3408
3401
  console.log(chalk3.dim(` Files: ${fingerprint.fileTree.length} found
3409
3402
  `));
3410
3403
  const targetAgent = options.agent || await promptAgent();
@@ -3413,12 +3406,16 @@ async function initCommand(options) {
3413
3406
  if (isEmpty) {
3414
3407
  fingerprint.description = await promptInput("What will you build in this project?");
3415
3408
  }
3416
- await enrichmentPromise;
3417
- console.log(chalk3.hex("#6366f1").bold(" Step 3/4 \u2014 Auditing your configs\n"));
3418
- console.log(chalk3.dim(" AI is auditing your CLAUDE.md, skills, and rules against your"));
3419
- console.log(chalk3.dim(" project's actual codebase and conventions.\n"));
3420
- console.log(chalk3.dim(" This usually takes 1\u20133 minutes on first run.\n"));
3421
3409
  const hasExistingConfig = !!(fingerprint.existingConfigs.claudeMd || fingerprint.existingConfigs.claudeSettings || fingerprint.existingConfigs.claudeSkills?.length || fingerprint.existingConfigs.cursorrules || fingerprint.existingConfigs.cursorRules?.length);
3410
+ if (hasExistingConfig) {
3411
+ console.log(chalk3.hex("#6366f1").bold(" Step 3/4 \u2014 Auditing your configs\n"));
3412
+ console.log(chalk3.dim(" AI is reviewing your existing configs against your codebase"));
3413
+ console.log(chalk3.dim(" and suggesting improvements.\n"));
3414
+ } else {
3415
+ console.log(chalk3.hex("#6366f1").bold(" Step 3/4 \u2014 Generating configs\n"));
3416
+ console.log(chalk3.dim(" AI is creating agent config files tailored to your project.\n"));
3417
+ }
3418
+ console.log(chalk3.dim(" This usually takes 1\u20133 minutes.\n"));
3422
3419
  const genStartTime = Date.now();
3423
3420
  const genSpinner = ora("Generating setup...").start();
3424
3421
  const genMessages = new SpinnerMessages(genSpinner, GENERATION_MESSAGES, { showElapsedTime: true });
@@ -3543,13 +3540,14 @@ async function initCommand(options) {
3543
3540
  lastRefreshTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
3544
3541
  targetAgent
3545
3542
  });
3546
- if (targetAgent === "claude" || targetAgent === "both") {
3543
+ const hookChoice = await promptHookType(targetAgent);
3544
+ if (hookChoice === "claude" || hookChoice === "both") {
3547
3545
  const hookResult = installHook();
3548
3546
  if (hookResult.installed) {
3549
- console.log(` ${chalk3.green("\u2713")} Auto-refresh hook installed \u2014 docs update on Claude Code session end`);
3547
+ console.log(` ${chalk3.green("\u2713")} Claude Code hook installed \u2014 docs update on session end`);
3550
3548
  console.log(chalk3.dim(" Run `caliber hooks remove` to disable"));
3551
3549
  } else if (hookResult.alreadyInstalled) {
3552
- console.log(chalk3.dim(" Auto-refresh hook already installed"));
3550
+ console.log(chalk3.dim(" Claude Code hook already installed"));
3553
3551
  }
3554
3552
  const learnResult = installLearningHooks();
3555
3553
  if (learnResult.installed) {
@@ -3559,6 +3557,20 @@ async function initCommand(options) {
3559
3557
  console.log(chalk3.dim(" Learning hooks already installed"));
3560
3558
  }
3561
3559
  }
3560
+ if (hookChoice === "precommit" || hookChoice === "both") {
3561
+ const precommitResult = installPreCommitHook();
3562
+ if (precommitResult.installed) {
3563
+ console.log(` ${chalk3.green("\u2713")} Pre-commit hook installed \u2014 docs refresh before each commit`);
3564
+ console.log(chalk3.dim(" Run `caliber hooks remove-precommit` to disable"));
3565
+ } else if (precommitResult.alreadyInstalled) {
3566
+ console.log(chalk3.dim(" Pre-commit hook already installed"));
3567
+ } else {
3568
+ console.log(chalk3.yellow(" Could not install pre-commit hook (not a git repository?)"));
3569
+ }
3570
+ }
3571
+ if (hookChoice === "skip") {
3572
+ console.log(chalk3.dim(" Skipped auto-refresh hooks. Run `caliber hooks install` later to enable."));
3573
+ }
3562
3574
  const afterScore = computeLocalScore(process.cwd(), targetAgent);
3563
3575
  displayScoreDelta(baselineScore, afterScore);
3564
3576
  console.log(chalk3.bold.green(" Setup complete! Your coding agent is now configured."));
@@ -3618,6 +3630,21 @@ async function promptAgent() {
3618
3630
  ]
3619
3631
  });
3620
3632
  }
3633
+ async function promptHookType(targetAgent) {
3634
+ const choices = [];
3635
+ if (targetAgent === "claude" || targetAgent === "both") {
3636
+ choices.push({ name: "Claude Code hook (auto-refresh on session end)", value: "claude" });
3637
+ }
3638
+ choices.push({ name: "Git pre-commit hook (refresh before each commit)", value: "precommit" });
3639
+ if (targetAgent === "claude" || targetAgent === "both") {
3640
+ choices.push({ name: "Both (Claude Code + pre-commit)", value: "both" });
3641
+ }
3642
+ choices.push({ name: "Skip for now", value: "skip" });
3643
+ return select({
3644
+ message: "How would you like to auto-refresh your docs?",
3645
+ choices
3646
+ });
3647
+ }
3621
3648
  async function promptWantsReview() {
3622
3649
  const answer = await select({
3623
3650
  message: "Would you like to review the diffs before deciding?",
@@ -3909,9 +3936,10 @@ async function regenerateCommand(options) {
3909
3936
  genMessages.start();
3910
3937
  let generatedSetup = null;
3911
3938
  try {
3939
+ const targetAgent = readState()?.targetAgent ?? "both";
3912
3940
  const result2 = await generateSetup(
3913
3941
  fingerprint,
3914
- "both",
3942
+ targetAgent,
3915
3943
  void 0,
3916
3944
  {
3917
3945
  onStatus: (status) => {
@@ -3961,7 +3989,7 @@ async function regenerateCommand(options) {
3961
3989
  // src/commands/recommend.ts
3962
3990
  import chalk7 from "chalk";
3963
3991
  import ora4 from "ora";
3964
- import { mkdirSync, writeFileSync } from "fs";
3992
+ import { mkdirSync, readFileSync as readFileSync7, existsSync as existsSync9, writeFileSync } from "fs";
3965
3993
  import { join as join8, dirname as dirname2 } from "path";
3966
3994
 
3967
3995
  // src/scanner/index.ts
@@ -4123,15 +4151,26 @@ async function searchSkills(technologies) {
4123
4151
  }
4124
4152
  return results;
4125
4153
  }
4154
+ function extractTopDeps() {
4155
+ const pkgPath = join8(process.cwd(), "package.json");
4156
+ if (!existsSync9(pkgPath)) return [];
4157
+ try {
4158
+ const pkg3 = JSON.parse(readFileSync7(pkgPath, "utf-8"));
4159
+ return Object.keys(pkg3.dependencies ?? {});
4160
+ } catch {
4161
+ return [];
4162
+ }
4163
+ }
4126
4164
  async function recommendCommand(options) {
4127
4165
  const fingerprint = collectFingerprint(process.cwd());
4128
4166
  const platforms = detectLocalPlatforms();
4129
- const technologies = [
4167
+ const technologies = [...new Set([
4130
4168
  ...fingerprint.languages,
4131
- ...fingerprint.frameworks
4132
- ].filter(Boolean);
4169
+ ...fingerprint.frameworks,
4170
+ ...extractTopDeps()
4171
+ ].filter(Boolean))];
4133
4172
  if (technologies.length === 0) {
4134
- console.log(chalk7.yellow("Could not detect any languages or frameworks. Try running from a project root."));
4173
+ console.log(chalk7.yellow("Could not detect any languages or dependencies. Try running from a project root."));
4135
4174
  throw new Error("__exit__");
4136
4175
  }
4137
4176
  const spinner = ora4("Searching for skills...").start();
@@ -4328,7 +4367,7 @@ import chalk9 from "chalk";
4328
4367
  import ora5 from "ora";
4329
4368
 
4330
4369
  // src/lib/git-diff.ts
4331
- import { execSync as execSync4 } from "child_process";
4370
+ import { execSync as execSync5 } from "child_process";
4332
4371
  var MAX_DIFF_BYTES = 1e5;
4333
4372
  var DOC_PATTERNS = [
4334
4373
  "CLAUDE.md",
@@ -4342,7 +4381,7 @@ function excludeArgs() {
4342
4381
  }
4343
4382
  function safeExec(cmd) {
4344
4383
  try {
4345
- return execSync4(cmd, {
4384
+ return execSync5(cmd, {
4346
4385
  encoding: "utf-8",
4347
4386
  stdio: ["pipe", "pipe", "pipe"],
4348
4387
  maxBuffer: 10 * 1024 * 1024
@@ -4628,7 +4667,7 @@ import chalk10 from "chalk";
4628
4667
  async function hooksInstallCommand() {
4629
4668
  const result = installHook();
4630
4669
  if (result.alreadyInstalled) {
4631
- console.log(chalk10.dim("Hook already installed."));
4670
+ console.log(chalk10.dim("Claude Code hook already installed."));
4632
4671
  return;
4633
4672
  }
4634
4673
  console.log(chalk10.green("\u2713") + " SessionEnd hook installed in .claude/settings.json");
@@ -4637,18 +4676,47 @@ async function hooksInstallCommand() {
4637
4676
  async function hooksRemoveCommand() {
4638
4677
  const result = removeHook();
4639
4678
  if (result.notFound) {
4640
- console.log(chalk10.dim("Hook not found."));
4679
+ console.log(chalk10.dim("Claude Code hook not found."));
4641
4680
  return;
4642
4681
  }
4643
4682
  console.log(chalk10.green("\u2713") + " SessionEnd hook removed from .claude/settings.json");
4644
4683
  }
4684
+ async function hooksInstallPrecommitCommand() {
4685
+ const result = installPreCommitHook();
4686
+ if (result.alreadyInstalled) {
4687
+ console.log(chalk10.dim("Pre-commit hook already installed."));
4688
+ return;
4689
+ }
4690
+ if (!result.installed) {
4691
+ console.log(chalk10.red("Failed to install pre-commit hook (not a git repository?)."));
4692
+ return;
4693
+ }
4694
+ console.log(chalk10.green("\u2713") + " Pre-commit hook installed in .git/hooks/pre-commit");
4695
+ console.log(chalk10.dim(" Docs will auto-refresh before each commit via LLM."));
4696
+ }
4697
+ async function hooksRemovePrecommitCommand() {
4698
+ const result = removePreCommitHook();
4699
+ if (result.notFound) {
4700
+ console.log(chalk10.dim("Pre-commit hook not found."));
4701
+ return;
4702
+ }
4703
+ console.log(chalk10.green("\u2713") + " Pre-commit hook removed from .git/hooks/pre-commit");
4704
+ }
4645
4705
  async function hooksStatusCommand() {
4646
- const installed = isHookInstalled();
4647
- if (installed) {
4648
- console.log(chalk10.green("\u2713") + " Auto-refresh hook is " + chalk10.green("installed"));
4706
+ const claudeInstalled = isHookInstalled();
4707
+ const precommitInstalled = isPreCommitHookInstalled();
4708
+ if (claudeInstalled) {
4709
+ console.log(chalk10.green("\u2713") + " Claude Code hook is " + chalk10.green("installed"));
4710
+ } else {
4711
+ console.log(chalk10.dim("\u2717") + " Claude Code hook is " + chalk10.yellow("not installed"));
4712
+ }
4713
+ if (precommitInstalled) {
4714
+ console.log(chalk10.green("\u2713") + " Pre-commit hook is " + chalk10.green("installed"));
4649
4715
  } else {
4650
- console.log(chalk10.dim("\u2717") + " Auto-refresh hook is " + chalk10.yellow("not installed"));
4651
- console.log(chalk10.dim(" Run `caliber hooks install` to enable auto-refresh on session end."));
4716
+ console.log(chalk10.dim("\u2717") + " Pre-commit hook is " + chalk10.yellow("not installed"));
4717
+ }
4718
+ if (!claudeInstalled && !precommitInstalled) {
4719
+ console.log(chalk10.dim("\n Run `caliber hooks install` or `caliber hooks install-precommit` to enable auto-refresh."));
4652
4720
  }
4653
4721
  }
4654
4722
 
@@ -5095,10 +5163,12 @@ program.command("config").description("Configure LLM provider, API key, and mode
5095
5163
  program.command("recommend").description("Discover and install skill recommendations").option("--generate", "Force fresh recommendation search").action(recommendCommand);
5096
5164
  program.command("score").description("Score your current agent config setup (deterministic, no network)").option("--json", "Output as JSON").option("--quiet", "One-line output for scripts/hooks").option("--agent <type>", "Target agent: claude, cursor, or both").action(scoreCommand);
5097
5165
  program.command("refresh").description("Update docs based on recent code changes").option("--quiet", "Suppress output (for use in hooks)").option("--dry-run", "Preview changes without writing files").action(refreshCommand);
5098
- var hooks = program.command("hooks").description("Manage Claude Code session hooks");
5099
- hooks.command("install").description("Install auto-refresh SessionEnd hook").action(hooksInstallCommand);
5100
- hooks.command("remove").description("Remove auto-refresh SessionEnd hook").action(hooksRemoveCommand);
5101
- hooks.command("status").description("Check if auto-refresh hook is installed").action(hooksStatusCommand);
5166
+ var hooks = program.command("hooks").description("Manage auto-refresh hooks (Claude Code and git pre-commit)");
5167
+ hooks.command("install").description("Install Claude Code SessionEnd auto-refresh hook").action(hooksInstallCommand);
5168
+ hooks.command("remove").description("Remove Claude Code SessionEnd auto-refresh hook").action(hooksRemoveCommand);
5169
+ hooks.command("install-precommit").description("Install git pre-commit hook for auto-refresh").action(hooksInstallPrecommitCommand);
5170
+ hooks.command("remove-precommit").description("Remove git pre-commit hook").action(hooksRemovePrecommitCommand);
5171
+ hooks.command("status").description("Check installed hooks status").action(hooksStatusCommand);
5102
5172
  var learn = program.command("learn").description("Session learning \u2014 observe tool usage and extract reusable instructions");
5103
5173
  learn.command("observe").description("Record a tool event from stdin (called by hooks)").option("--failure", "Mark event as a tool failure").action(learnObserveCommand);
5104
5174
  learn.command("finalize").description("Analyze session events and update CLAUDE.md (called on SessionEnd)").action(learnFinalizeCommand);
@@ -5110,7 +5180,7 @@ learn.command("status").description("Show learning system status").action(learnS
5110
5180
  import fs25 from "fs";
5111
5181
  import path22 from "path";
5112
5182
  import { fileURLToPath as fileURLToPath2 } from "url";
5113
- import { execSync as execSync5 } from "child_process";
5183
+ import { execSync as execSync6 } from "child_process";
5114
5184
  import chalk13 from "chalk";
5115
5185
  import ora6 from "ora";
5116
5186
  import confirm2 from "@inquirer/confirm";
@@ -5120,7 +5190,7 @@ var pkg2 = JSON.parse(
5120
5190
  );
5121
5191
  function getInstalledVersion() {
5122
5192
  try {
5123
- const globalRoot = execSync5("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
5193
+ const globalRoot = execSync6("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
5124
5194
  const pkgPath = path22.join(globalRoot, "@rely-ai", "caliber", "package.json");
5125
5195
  return JSON.parse(fs25.readFileSync(pkgPath, "utf-8")).version;
5126
5196
  } catch {
@@ -5165,7 +5235,7 @@ Update available: ${current} -> ${latest}`)
5165
5235
  }
5166
5236
  const spinner = ora6("Updating caliber...").start();
5167
5237
  try {
5168
- execSync5(`npm install -g @rely-ai/caliber@${latest} --prefer-online`, { stdio: "pipe", timeout: 6e4 });
5238
+ execSync6(`npm install -g @rely-ai/caliber@${latest} --prefer-online`, { stdio: "pipe", timeout: 6e4 });
5169
5239
  const installed = getInstalledVersion();
5170
5240
  if (installed !== latest) {
5171
5241
  spinner.fail(`Update incomplete \u2014 got ${installed ?? "unknown"}, expected ${latest}`);
@@ -5178,7 +5248,7 @@ Update available: ${current} -> ${latest}`)
5178
5248
  console.log(chalk13.dim(`
5179
5249
  Restarting: caliber ${args.join(" ")}
5180
5250
  `));
5181
- execSync5(`caliber ${args.map((a) => JSON.stringify(a)).join(" ")}`, {
5251
+ execSync6(`caliber ${args.map((a) => JSON.stringify(a)).join(" ")}`, {
5182
5252
  stdio: "inherit",
5183
5253
  env: { ...process.env, CALIBER_SKIP_UPDATE_CHECK: "1" }
5184
5254
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rely-ai/caliber",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Open-source CLI for configuring coding agent environments (CLAUDE.md, .cursorrules, skills). Bring your own LLM.",
5
5
  "type": "module",
6
6
  "bin": {