@rely-ai/caliber 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +15 -5
  2. package/dist/bin.js +169 -116
  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);
@@ -3401,10 +3395,9 @@ async function initCommand(options) {
3401
3395
  console.log(chalk3.dim(" Detecting languages, frameworks, 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,7 +3406,6 @@ 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
3409
  console.log(chalk3.hex("#6366f1").bold(" Step 3/4 \u2014 Auditing your configs\n"));
3418
3410
  console.log(chalk3.dim(" AI is auditing your CLAUDE.md, skills, and rules against your"));
3419
3411
  console.log(chalk3.dim(" project's actual codebase and conventions.\n"));
@@ -3543,13 +3535,14 @@ async function initCommand(options) {
3543
3535
  lastRefreshTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
3544
3536
  targetAgent
3545
3537
  });
3546
- if (targetAgent === "claude" || targetAgent === "both") {
3538
+ const hookChoice = await promptHookType(targetAgent);
3539
+ if (hookChoice === "claude" || hookChoice === "both") {
3547
3540
  const hookResult = installHook();
3548
3541
  if (hookResult.installed) {
3549
- console.log(` ${chalk3.green("\u2713")} Auto-refresh hook installed \u2014 docs update on Claude Code session end`);
3542
+ console.log(` ${chalk3.green("\u2713")} Claude Code hook installed \u2014 docs update on session end`);
3550
3543
  console.log(chalk3.dim(" Run `caliber hooks remove` to disable"));
3551
3544
  } else if (hookResult.alreadyInstalled) {
3552
- console.log(chalk3.dim(" Auto-refresh hook already installed"));
3545
+ console.log(chalk3.dim(" Claude Code hook already installed"));
3553
3546
  }
3554
3547
  const learnResult = installLearningHooks();
3555
3548
  if (learnResult.installed) {
@@ -3559,6 +3552,20 @@ async function initCommand(options) {
3559
3552
  console.log(chalk3.dim(" Learning hooks already installed"));
3560
3553
  }
3561
3554
  }
3555
+ if (hookChoice === "precommit" || hookChoice === "both") {
3556
+ const precommitResult = installPreCommitHook();
3557
+ if (precommitResult.installed) {
3558
+ console.log(` ${chalk3.green("\u2713")} Pre-commit hook installed \u2014 docs refresh before each commit`);
3559
+ console.log(chalk3.dim(" Run `caliber hooks remove-precommit` to disable"));
3560
+ } else if (precommitResult.alreadyInstalled) {
3561
+ console.log(chalk3.dim(" Pre-commit hook already installed"));
3562
+ } else {
3563
+ console.log(chalk3.yellow(" Could not install pre-commit hook (not a git repository?)"));
3564
+ }
3565
+ }
3566
+ if (hookChoice === "skip") {
3567
+ console.log(chalk3.dim(" Skipped auto-refresh hooks. Run `caliber hooks install` later to enable."));
3568
+ }
3562
3569
  const afterScore = computeLocalScore(process.cwd(), targetAgent);
3563
3570
  displayScoreDelta(baselineScore, afterScore);
3564
3571
  console.log(chalk3.bold.green(" Setup complete! Your coding agent is now configured."));
@@ -3618,6 +3625,21 @@ async function promptAgent() {
3618
3625
  ]
3619
3626
  });
3620
3627
  }
3628
+ async function promptHookType(targetAgent) {
3629
+ const choices = [];
3630
+ if (targetAgent === "claude" || targetAgent === "both") {
3631
+ choices.push({ name: "Claude Code hook (auto-refresh on session end)", value: "claude" });
3632
+ }
3633
+ choices.push({ name: "Git pre-commit hook (refresh before each commit)", value: "precommit" });
3634
+ if (targetAgent === "claude" || targetAgent === "both") {
3635
+ choices.push({ name: "Both (Claude Code + pre-commit)", value: "both" });
3636
+ }
3637
+ choices.push({ name: "Skip for now", value: "skip" });
3638
+ return select({
3639
+ message: "How would you like to auto-refresh your docs?",
3640
+ choices
3641
+ });
3642
+ }
3621
3643
  async function promptWantsReview() {
3622
3644
  const answer = await select({
3623
3645
  message: "Would you like to review the diffs before deciding?",
@@ -4328,7 +4350,7 @@ import chalk9 from "chalk";
4328
4350
  import ora5 from "ora";
4329
4351
 
4330
4352
  // src/lib/git-diff.ts
4331
- import { execSync as execSync4 } from "child_process";
4353
+ import { execSync as execSync5 } from "child_process";
4332
4354
  var MAX_DIFF_BYTES = 1e5;
4333
4355
  var DOC_PATTERNS = [
4334
4356
  "CLAUDE.md",
@@ -4342,7 +4364,7 @@ function excludeArgs() {
4342
4364
  }
4343
4365
  function safeExec(cmd) {
4344
4366
  try {
4345
- return execSync4(cmd, {
4367
+ return execSync5(cmd, {
4346
4368
  encoding: "utf-8",
4347
4369
  stdio: ["pipe", "pipe", "pipe"],
4348
4370
  maxBuffer: 10 * 1024 * 1024
@@ -4628,7 +4650,7 @@ import chalk10 from "chalk";
4628
4650
  async function hooksInstallCommand() {
4629
4651
  const result = installHook();
4630
4652
  if (result.alreadyInstalled) {
4631
- console.log(chalk10.dim("Hook already installed."));
4653
+ console.log(chalk10.dim("Claude Code hook already installed."));
4632
4654
  return;
4633
4655
  }
4634
4656
  console.log(chalk10.green("\u2713") + " SessionEnd hook installed in .claude/settings.json");
@@ -4637,18 +4659,47 @@ async function hooksInstallCommand() {
4637
4659
  async function hooksRemoveCommand() {
4638
4660
  const result = removeHook();
4639
4661
  if (result.notFound) {
4640
- console.log(chalk10.dim("Hook not found."));
4662
+ console.log(chalk10.dim("Claude Code hook not found."));
4641
4663
  return;
4642
4664
  }
4643
4665
  console.log(chalk10.green("\u2713") + " SessionEnd hook removed from .claude/settings.json");
4644
4666
  }
4667
+ async function hooksInstallPrecommitCommand() {
4668
+ const result = installPreCommitHook();
4669
+ if (result.alreadyInstalled) {
4670
+ console.log(chalk10.dim("Pre-commit hook already installed."));
4671
+ return;
4672
+ }
4673
+ if (!result.installed) {
4674
+ console.log(chalk10.red("Failed to install pre-commit hook (not a git repository?)."));
4675
+ return;
4676
+ }
4677
+ console.log(chalk10.green("\u2713") + " Pre-commit hook installed in .git/hooks/pre-commit");
4678
+ console.log(chalk10.dim(" Docs will auto-refresh before each commit via LLM."));
4679
+ }
4680
+ async function hooksRemovePrecommitCommand() {
4681
+ const result = removePreCommitHook();
4682
+ if (result.notFound) {
4683
+ console.log(chalk10.dim("Pre-commit hook not found."));
4684
+ return;
4685
+ }
4686
+ console.log(chalk10.green("\u2713") + " Pre-commit hook removed from .git/hooks/pre-commit");
4687
+ }
4645
4688
  async function hooksStatusCommand() {
4646
- const installed = isHookInstalled();
4647
- if (installed) {
4648
- console.log(chalk10.green("\u2713") + " Auto-refresh hook is " + chalk10.green("installed"));
4689
+ const claudeInstalled = isHookInstalled();
4690
+ const precommitInstalled = isPreCommitHookInstalled();
4691
+ if (claudeInstalled) {
4692
+ console.log(chalk10.green("\u2713") + " Claude Code hook is " + chalk10.green("installed"));
4649
4693
  } 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."));
4694
+ console.log(chalk10.dim("\u2717") + " Claude Code hook is " + chalk10.yellow("not installed"));
4695
+ }
4696
+ if (precommitInstalled) {
4697
+ console.log(chalk10.green("\u2713") + " Pre-commit hook is " + chalk10.green("installed"));
4698
+ } else {
4699
+ console.log(chalk10.dim("\u2717") + " Pre-commit hook is " + chalk10.yellow("not installed"));
4700
+ }
4701
+ if (!claudeInstalled && !precommitInstalled) {
4702
+ console.log(chalk10.dim("\n Run `caliber hooks install` or `caliber hooks install-precommit` to enable auto-refresh."));
4652
4703
  }
4653
4704
  }
4654
4705
 
@@ -5095,10 +5146,12 @@ program.command("config").description("Configure LLM provider, API key, and mode
5095
5146
  program.command("recommend").description("Discover and install skill recommendations").option("--generate", "Force fresh recommendation search").action(recommendCommand);
5096
5147
  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
5148
  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);
5149
+ var hooks = program.command("hooks").description("Manage auto-refresh hooks (Claude Code and git pre-commit)");
5150
+ hooks.command("install").description("Install Claude Code SessionEnd auto-refresh hook").action(hooksInstallCommand);
5151
+ hooks.command("remove").description("Remove Claude Code SessionEnd auto-refresh hook").action(hooksRemoveCommand);
5152
+ hooks.command("install-precommit").description("Install git pre-commit hook for auto-refresh").action(hooksInstallPrecommitCommand);
5153
+ hooks.command("remove-precommit").description("Remove git pre-commit hook").action(hooksRemovePrecommitCommand);
5154
+ hooks.command("status").description("Check installed hooks status").action(hooksStatusCommand);
5102
5155
  var learn = program.command("learn").description("Session learning \u2014 observe tool usage and extract reusable instructions");
5103
5156
  learn.command("observe").description("Record a tool event from stdin (called by hooks)").option("--failure", "Mark event as a tool failure").action(learnObserveCommand);
5104
5157
  learn.command("finalize").description("Analyze session events and update CLAUDE.md (called on SessionEnd)").action(learnFinalizeCommand);
@@ -5110,7 +5163,7 @@ learn.command("status").description("Show learning system status").action(learnS
5110
5163
  import fs25 from "fs";
5111
5164
  import path22 from "path";
5112
5165
  import { fileURLToPath as fileURLToPath2 } from "url";
5113
- import { execSync as execSync5 } from "child_process";
5166
+ import { execSync as execSync6 } from "child_process";
5114
5167
  import chalk13 from "chalk";
5115
5168
  import ora6 from "ora";
5116
5169
  import confirm2 from "@inquirer/confirm";
@@ -5120,7 +5173,7 @@ var pkg2 = JSON.parse(
5120
5173
  );
5121
5174
  function getInstalledVersion() {
5122
5175
  try {
5123
- const globalRoot = execSync5("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
5176
+ const globalRoot = execSync6("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
5124
5177
  const pkgPath = path22.join(globalRoot, "@rely-ai", "caliber", "package.json");
5125
5178
  return JSON.parse(fs25.readFileSync(pkgPath, "utf-8")).version;
5126
5179
  } catch {
@@ -5165,7 +5218,7 @@ Update available: ${current} -> ${latest}`)
5165
5218
  }
5166
5219
  const spinner = ora6("Updating caliber...").start();
5167
5220
  try {
5168
- execSync5(`npm install -g @rely-ai/caliber@${latest} --prefer-online`, { stdio: "pipe", timeout: 6e4 });
5221
+ execSync6(`npm install -g @rely-ai/caliber@${latest} --prefer-online`, { stdio: "pipe", timeout: 6e4 });
5169
5222
  const installed = getInstalledVersion();
5170
5223
  if (installed !== latest) {
5171
5224
  spinner.fail(`Update incomplete \u2014 got ${installed ?? "unknown"}, expected ${latest}`);
@@ -5178,7 +5231,7 @@ Update available: ${current} -> ${latest}`)
5178
5231
  console.log(chalk13.dim(`
5179
5232
  Restarting: caliber ${args.join(" ")}
5180
5233
  `));
5181
- execSync5(`caliber ${args.map((a) => JSON.stringify(a)).join(" ")}`, {
5234
+ execSync6(`caliber ${args.map((a) => JSON.stringify(a)).join(" ")}`, {
5182
5235
  stdio: "inherit",
5183
5236
  env: { ...process.env, CALIBER_SKIP_UPDATE_CHECK: "1" }
5184
5237
  });
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.0",
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": {