@caliber-ai/cli 0.22.0 → 0.24.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.
package/dist/bin.js CHANGED
@@ -30,11 +30,15 @@ __export(constants_exports, {
30
30
  CALIBER_DIR: () => CALIBER_DIR,
31
31
  CLI_CALLBACK_PORT: () => CLI_CALLBACK_PORT,
32
32
  FRONTEND_URL: () => FRONTEND_URL,
33
+ LEARNING_DIR: () => LEARNING_DIR,
34
+ LEARNING_MAX_EVENTS: () => LEARNING_MAX_EVENTS,
35
+ LEARNING_SESSION_FILE: () => LEARNING_SESSION_FILE,
36
+ LEARNING_STATE_FILE: () => LEARNING_STATE_FILE,
33
37
  MANIFEST_FILE: () => MANIFEST_FILE
34
38
  });
35
39
  import path2 from "path";
36
40
  import os2 from "os";
37
- var API_URL, FRONTEND_URL, CLI_CALLBACK_PORT, AUTH_DIR, AUTH_FILE, CALIBER_DIR, MANIFEST_FILE, BACKUPS_DIR;
41
+ var API_URL, FRONTEND_URL, CLI_CALLBACK_PORT, AUTH_DIR, AUTH_FILE, CALIBER_DIR, MANIFEST_FILE, BACKUPS_DIR, LEARNING_DIR, LEARNING_SESSION_FILE, LEARNING_STATE_FILE, LEARNING_MAX_EVENTS;
38
42
  var init_constants = __esm({
39
43
  "src/constants.ts"() {
40
44
  "use strict";
@@ -46,6 +50,10 @@ var init_constants = __esm({
46
50
  CALIBER_DIR = ".caliber";
47
51
  MANIFEST_FILE = path2.join(CALIBER_DIR, "manifest.json");
48
52
  BACKUPS_DIR = path2.join(CALIBER_DIR, "backups");
53
+ LEARNING_DIR = path2.join(CALIBER_DIR, "learning");
54
+ LEARNING_SESSION_FILE = "current-session.jsonl";
55
+ LEARNING_STATE_FILE = "state.json";
56
+ LEARNING_MAX_EVENTS = 500;
49
57
  }
50
58
  });
51
59
 
@@ -75,8 +83,8 @@ if (dsn) {
75
83
 
76
84
  // src/cli.ts
77
85
  import { Command } from "commander";
78
- import fs22 from "fs";
79
- import path19 from "path";
86
+ import fs25 from "fs";
87
+ import path22 from "path";
80
88
  import { fileURLToPath as fileURLToPath3 } from "url";
81
89
 
82
90
  // src/commands/init.ts
@@ -84,7 +92,7 @@ import chalk3 from "chalk";
84
92
  import ora2 from "ora";
85
93
  import readline from "readline";
86
94
  import select from "@inquirer/select";
87
- import fs17 from "fs";
95
+ import fs18 from "fs";
88
96
 
89
97
  // src/auth/token-store.ts
90
98
  init_constants();
@@ -957,9 +965,9 @@ async function getValidToken() {
957
965
  }
958
966
  return refreshed;
959
967
  }
960
- async function apiRequest(path21, options = {}) {
968
+ async function apiRequest(path24, options = {}) {
961
969
  let token = await getValidToken();
962
- let resp = await fetch(`${API_URL}${path21}`, {
970
+ let resp = await fetch(`${API_URL}${path24}`, {
963
971
  method: options.method || "GET",
964
972
  headers: {
965
973
  "Content-Type": "application/json",
@@ -973,7 +981,7 @@ async function apiRequest(path21, options = {}) {
973
981
  throw new Error("Session expired. Run `caliber login` to re-authenticate.");
974
982
  }
975
983
  token = refreshed;
976
- resp = await fetch(`${API_URL}${path21}`, {
984
+ resp = await fetch(`${API_URL}${path24}`, {
977
985
  method: options.method || "GET",
978
986
  headers: {
979
987
  "Content-Type": "application/json",
@@ -989,9 +997,9 @@ async function apiRequest(path21, options = {}) {
989
997
  const json = await resp.json();
990
998
  return json.data;
991
999
  }
992
- async function apiStream(path21, body, onChunk, onComplete, onError, onStatus) {
1000
+ async function apiStream(path24, body, onChunk, onComplete, onError, onStatus) {
993
1001
  let token = await getValidToken();
994
- let resp = await fetch(`${API_URL}${path21}`, {
1002
+ let resp = await fetch(`${API_URL}${path24}`, {
995
1003
  method: "POST",
996
1004
  headers: {
997
1005
  "Content-Type": "application/json",
@@ -1005,7 +1013,7 @@ async function apiStream(path21, body, onChunk, onComplete, onError, onStatus) {
1005
1013
  throw new Error("Session expired. Run `caliber login` to re-authenticate.");
1006
1014
  }
1007
1015
  token = refreshed;
1008
- resp = await fetch(`${API_URL}${path21}`, {
1016
+ resp = await fetch(`${API_URL}${path24}`, {
1009
1017
  method: "POST",
1010
1018
  headers: {
1011
1019
  "Content-Type": "application/json",
@@ -1510,25 +1518,112 @@ function removeHook() {
1510
1518
  return { removed: true, notFound: false };
1511
1519
  }
1512
1520
 
1513
- // src/lib/state.ts
1514
- init_constants();
1521
+ // src/lib/learning-hooks.ts
1515
1522
  import fs16 from "fs";
1516
1523
  import path15 from "path";
1524
+ var SETTINGS_PATH2 = path15.join(".claude", "settings.json");
1525
+ var HOOK_CONFIGS = [
1526
+ {
1527
+ event: "PostToolUse",
1528
+ command: "caliber learn observe",
1529
+ description: "Caliber: recording tool usage for session learning"
1530
+ },
1531
+ {
1532
+ event: "PostToolUseFailure",
1533
+ command: "caliber learn observe --failure",
1534
+ description: "Caliber: recording tool failure for session learning"
1535
+ },
1536
+ {
1537
+ event: "SessionEnd",
1538
+ command: "caliber learn finalize",
1539
+ description: "Caliber: finalizing session learnings"
1540
+ }
1541
+ ];
1542
+ function readSettings2() {
1543
+ if (!fs16.existsSync(SETTINGS_PATH2)) return {};
1544
+ try {
1545
+ return JSON.parse(fs16.readFileSync(SETTINGS_PATH2, "utf-8"));
1546
+ } catch {
1547
+ return {};
1548
+ }
1549
+ }
1550
+ function writeSettings2(settings) {
1551
+ const dir = path15.dirname(SETTINGS_PATH2);
1552
+ if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
1553
+ fs16.writeFileSync(SETTINGS_PATH2, JSON.stringify(settings, null, 2));
1554
+ }
1555
+ function hasLearningHook(matchers, command) {
1556
+ return matchers.some((entry) => entry.hooks?.some((h) => h.command === command));
1557
+ }
1558
+ function areLearningHooksInstalled() {
1559
+ const settings = readSettings2();
1560
+ if (!settings.hooks) return false;
1561
+ return HOOK_CONFIGS.every((cfg) => {
1562
+ const matchers = settings.hooks[cfg.event];
1563
+ return Array.isArray(matchers) && hasLearningHook(matchers, cfg.command);
1564
+ });
1565
+ }
1566
+ function installLearningHooks() {
1567
+ if (areLearningHooksInstalled()) {
1568
+ return { installed: false, alreadyInstalled: true };
1569
+ }
1570
+ const settings = readSettings2();
1571
+ if (!settings.hooks) settings.hooks = {};
1572
+ for (const cfg of HOOK_CONFIGS) {
1573
+ if (!Array.isArray(settings.hooks[cfg.event])) {
1574
+ settings.hooks[cfg.event] = [];
1575
+ }
1576
+ if (!hasLearningHook(settings.hooks[cfg.event], cfg.command)) {
1577
+ settings.hooks[cfg.event].push({
1578
+ matcher: "",
1579
+ hooks: [{ type: "command", command: cfg.command, description: cfg.description }]
1580
+ });
1581
+ }
1582
+ }
1583
+ writeSettings2(settings);
1584
+ return { installed: true, alreadyInstalled: false };
1585
+ }
1586
+ function removeLearningHooks() {
1587
+ const settings = readSettings2();
1588
+ if (!settings.hooks) return { removed: false, notFound: true };
1589
+ let removedAny = false;
1590
+ for (const cfg of HOOK_CONFIGS) {
1591
+ const matchers = settings.hooks[cfg.event];
1592
+ if (!Array.isArray(matchers)) continue;
1593
+ const idx = matchers.findIndex((entry) => entry.hooks?.some((h) => h.command === cfg.command));
1594
+ if (idx !== -1) {
1595
+ matchers.splice(idx, 1);
1596
+ removedAny = true;
1597
+ if (matchers.length === 0) delete settings.hooks[cfg.event];
1598
+ }
1599
+ }
1600
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) {
1601
+ delete settings.hooks;
1602
+ }
1603
+ if (!removedAny) return { removed: false, notFound: true };
1604
+ writeSettings2(settings);
1605
+ return { removed: true, notFound: false };
1606
+ }
1607
+
1608
+ // src/lib/state.ts
1609
+ init_constants();
1610
+ import fs17 from "fs";
1611
+ import path16 from "path";
1517
1612
  import { execSync as execSync3 } from "child_process";
1518
- var STATE_FILE = path15.join(CALIBER_DIR, ".caliber-state.json");
1613
+ var STATE_FILE = path16.join(CALIBER_DIR, ".caliber-state.json");
1519
1614
  function readState() {
1520
1615
  try {
1521
- if (!fs16.existsSync(STATE_FILE)) return null;
1522
- return JSON.parse(fs16.readFileSync(STATE_FILE, "utf-8"));
1616
+ if (!fs17.existsSync(STATE_FILE)) return null;
1617
+ return JSON.parse(fs17.readFileSync(STATE_FILE, "utf-8"));
1523
1618
  } catch {
1524
1619
  return null;
1525
1620
  }
1526
1621
  }
1527
1622
  function writeState(state) {
1528
- if (!fs16.existsSync(CALIBER_DIR)) {
1529
- fs16.mkdirSync(CALIBER_DIR, { recursive: true });
1623
+ if (!fs17.existsSync(CALIBER_DIR)) {
1624
+ fs17.mkdirSync(CALIBER_DIR, { recursive: true });
1530
1625
  }
1531
- fs16.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
1626
+ fs17.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
1532
1627
  }
1533
1628
  function getCurrentHeadSha() {
1534
1629
  try {
@@ -1885,6 +1980,13 @@ async function initCommand(options) {
1885
1980
  } else if (hookResult.alreadyInstalled) {
1886
1981
  console.log(chalk3.dim(" Auto-refresh hook already installed"));
1887
1982
  }
1983
+ const learnResult = installLearningHooks();
1984
+ if (learnResult.installed) {
1985
+ console.log(` ${chalk3.green("\u2713")} Learning hooks installed \u2014 session insights captured automatically`);
1986
+ console.log(chalk3.dim(" Run `caliber learn remove` to disable"));
1987
+ } else if (learnResult.alreadyInstalled) {
1988
+ console.log(chalk3.dim(" Learning hooks already installed"));
1989
+ }
1888
1990
  }
1889
1991
  try {
1890
1992
  let projectId = existingProjectId;
@@ -2016,8 +2118,8 @@ function openReview(method, stagedFiles) {
2016
2118
  } else {
2017
2119
  for (const file of stagedFiles) {
2018
2120
  if (file.currentPath) {
2019
- const currentLines = fs17.readFileSync(file.currentPath, "utf-8").split("\n");
2020
- const proposedLines = fs17.readFileSync(file.proposedPath, "utf-8").split("\n");
2121
+ const currentLines = fs18.readFileSync(file.currentPath, "utf-8").split("\n");
2122
+ const proposedLines = fs18.readFileSync(file.proposedPath, "utf-8").split("\n");
2021
2123
  const patch = createTwoFilesPatch(file.relativePath, file.relativePath, currentLines.join("\n"), proposedLines.join("\n"));
2022
2124
  let added = 0, removed = 0;
2023
2125
  for (const line of patch.split("\n")) {
@@ -2026,7 +2128,7 @@ function openReview(method, stagedFiles) {
2026
2128
  }
2027
2129
  console.log(` ${chalk3.yellow("~")} ${file.relativePath} ${chalk3.green(`+${added}`)} ${chalk3.red(`-${removed}`)}`);
2028
2130
  } else {
2029
- const lines = fs17.readFileSync(file.proposedPath, "utf-8").split("\n").length;
2131
+ const lines = fs18.readFileSync(file.proposedPath, "utf-8").split("\n").length;
2030
2132
  console.log(` ${chalk3.green("+")} ${file.relativePath} ${chalk3.dim(`${lines} lines`)}`);
2031
2133
  }
2032
2134
  }
@@ -2057,7 +2159,7 @@ function printSetupSummary(setup) {
2057
2159
  };
2058
2160
  if (claude) {
2059
2161
  if (claude.claudeMd) {
2060
- const icon = fs17.existsSync("CLAUDE.md") ? chalk3.yellow("~") : chalk3.green("+");
2162
+ const icon = fs18.existsSync("CLAUDE.md") ? chalk3.yellow("~") : chalk3.green("+");
2061
2163
  const desc = getDescription("CLAUDE.md");
2062
2164
  console.log(` ${icon} ${chalk3.bold("CLAUDE.md")}`);
2063
2165
  if (desc) {
@@ -2069,7 +2171,7 @@ function printSetupSummary(setup) {
2069
2171
  if (Array.isArray(skills) && skills.length > 0) {
2070
2172
  for (const skill of skills) {
2071
2173
  const skillPath = `.claude/skills/${skill.name}/SKILL.md`;
2072
- const icon = fs17.existsSync(skillPath) ? chalk3.yellow("~") : chalk3.green("+");
2174
+ const icon = fs18.existsSync(skillPath) ? chalk3.yellow("~") : chalk3.green("+");
2073
2175
  const desc = getDescription(skillPath);
2074
2176
  console.log(` ${icon} ${chalk3.bold(skillPath)}`);
2075
2177
  console.log(chalk3.dim(` ${desc || skill.description || skill.name}`));
@@ -2079,7 +2181,7 @@ function printSetupSummary(setup) {
2079
2181
  }
2080
2182
  if (cursor) {
2081
2183
  if (cursor.cursorrules) {
2082
- const icon = fs17.existsSync(".cursorrules") ? chalk3.yellow("~") : chalk3.green("+");
2184
+ const icon = fs18.existsSync(".cursorrules") ? chalk3.yellow("~") : chalk3.green("+");
2083
2185
  const desc = getDescription(".cursorrules");
2084
2186
  console.log(` ${icon} ${chalk3.bold(".cursorrules")}`);
2085
2187
  if (desc) console.log(chalk3.dim(` ${desc}`));
@@ -2089,7 +2191,7 @@ function printSetupSummary(setup) {
2089
2191
  if (Array.isArray(cursorSkills) && cursorSkills.length > 0) {
2090
2192
  for (const skill of cursorSkills) {
2091
2193
  const skillPath = `.cursor/skills/${skill.name}/SKILL.md`;
2092
- const icon = fs17.existsSync(skillPath) ? chalk3.yellow("~") : chalk3.green("+");
2194
+ const icon = fs18.existsSync(skillPath) ? chalk3.yellow("~") : chalk3.green("+");
2093
2195
  const desc = getDescription(skillPath);
2094
2196
  console.log(` ${icon} ${chalk3.bold(skillPath)}`);
2095
2197
  console.log(chalk3.dim(` ${desc || skill.description || skill.name}`));
@@ -2100,7 +2202,7 @@ function printSetupSummary(setup) {
2100
2202
  if (Array.isArray(rules) && rules.length > 0) {
2101
2203
  for (const rule of rules) {
2102
2204
  const rulePath = `.cursor/rules/${rule.filename}`;
2103
- const icon = fs17.existsSync(rulePath) ? chalk3.yellow("~") : chalk3.green("+");
2205
+ const icon = fs18.existsSync(rulePath) ? chalk3.yellow("~") : chalk3.green("+");
2104
2206
  const desc = getDescription(rulePath);
2105
2207
  console.log(` ${icon} ${chalk3.bold(rulePath)}`);
2106
2208
  if (desc) {
@@ -2197,16 +2299,16 @@ function undoCommand() {
2197
2299
 
2198
2300
  // src/commands/status.ts
2199
2301
  import chalk5 from "chalk";
2200
- import fs19 from "fs";
2302
+ import fs20 from "fs";
2201
2303
 
2202
2304
  // src/scanner/index.ts
2203
- import fs18 from "fs";
2204
- import path16 from "path";
2305
+ import fs19 from "fs";
2306
+ import path17 from "path";
2205
2307
  import crypto4 from "crypto";
2206
2308
  function scanLocalState(dir) {
2207
2309
  const items = [];
2208
- const claudeMdPath = path16.join(dir, "CLAUDE.md");
2209
- if (fs18.existsSync(claudeMdPath)) {
2310
+ const claudeMdPath = path17.join(dir, "CLAUDE.md");
2311
+ if (fs19.existsSync(claudeMdPath)) {
2210
2312
  items.push({
2211
2313
  type: "rule",
2212
2314
  platform: "claude",
@@ -2215,10 +2317,10 @@ function scanLocalState(dir) {
2215
2317
  path: claudeMdPath
2216
2318
  });
2217
2319
  }
2218
- const skillsDir = path16.join(dir, ".claude", "skills");
2219
- if (fs18.existsSync(skillsDir)) {
2220
- for (const file of fs18.readdirSync(skillsDir).filter((f) => f.endsWith(".md"))) {
2221
- const filePath = path16.join(skillsDir, file);
2320
+ const skillsDir = path17.join(dir, ".claude", "skills");
2321
+ if (fs19.existsSync(skillsDir)) {
2322
+ for (const file of fs19.readdirSync(skillsDir).filter((f) => f.endsWith(".md"))) {
2323
+ const filePath = path17.join(skillsDir, file);
2222
2324
  items.push({
2223
2325
  type: "skill",
2224
2326
  platform: "claude",
@@ -2228,10 +2330,10 @@ function scanLocalState(dir) {
2228
2330
  });
2229
2331
  }
2230
2332
  }
2231
- const mcpJsonPath = path16.join(dir, ".mcp.json");
2232
- if (fs18.existsSync(mcpJsonPath)) {
2333
+ const mcpJsonPath = path17.join(dir, ".mcp.json");
2334
+ if (fs19.existsSync(mcpJsonPath)) {
2233
2335
  try {
2234
- const mcpJson = JSON.parse(fs18.readFileSync(mcpJsonPath, "utf-8"));
2336
+ const mcpJson = JSON.parse(fs19.readFileSync(mcpJsonPath, "utf-8"));
2235
2337
  if (mcpJson.mcpServers) {
2236
2338
  for (const name of Object.keys(mcpJson.mcpServers)) {
2237
2339
  items.push({
@@ -2246,8 +2348,8 @@ function scanLocalState(dir) {
2246
2348
  } catch {
2247
2349
  }
2248
2350
  }
2249
- const cursorrulesPath = path16.join(dir, ".cursorrules");
2250
- if (fs18.existsSync(cursorrulesPath)) {
2351
+ const cursorrulesPath = path17.join(dir, ".cursorrules");
2352
+ if (fs19.existsSync(cursorrulesPath)) {
2251
2353
  items.push({
2252
2354
  type: "rule",
2253
2355
  platform: "cursor",
@@ -2256,10 +2358,10 @@ function scanLocalState(dir) {
2256
2358
  path: cursorrulesPath
2257
2359
  });
2258
2360
  }
2259
- const cursorRulesDir = path16.join(dir, ".cursor", "rules");
2260
- if (fs18.existsSync(cursorRulesDir)) {
2261
- for (const file of fs18.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"))) {
2262
- const filePath = path16.join(cursorRulesDir, file);
2361
+ const cursorRulesDir = path17.join(dir, ".cursor", "rules");
2362
+ if (fs19.existsSync(cursorRulesDir)) {
2363
+ for (const file of fs19.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"))) {
2364
+ const filePath = path17.join(cursorRulesDir, file);
2263
2365
  items.push({
2264
2366
  type: "rule",
2265
2367
  platform: "cursor",
@@ -2269,12 +2371,12 @@ function scanLocalState(dir) {
2269
2371
  });
2270
2372
  }
2271
2373
  }
2272
- const cursorSkillsDir = path16.join(dir, ".cursor", "skills");
2273
- if (fs18.existsSync(cursorSkillsDir)) {
2374
+ const cursorSkillsDir = path17.join(dir, ".cursor", "skills");
2375
+ if (fs19.existsSync(cursorSkillsDir)) {
2274
2376
  try {
2275
- for (const name of fs18.readdirSync(cursorSkillsDir)) {
2276
- const skillFile = path16.join(cursorSkillsDir, name, "SKILL.md");
2277
- if (fs18.existsSync(skillFile)) {
2377
+ for (const name of fs19.readdirSync(cursorSkillsDir)) {
2378
+ const skillFile = path17.join(cursorSkillsDir, name, "SKILL.md");
2379
+ if (fs19.existsSync(skillFile)) {
2278
2380
  items.push({
2279
2381
  type: "skill",
2280
2382
  platform: "cursor",
@@ -2287,10 +2389,10 @@ function scanLocalState(dir) {
2287
2389
  } catch {
2288
2390
  }
2289
2391
  }
2290
- const cursorMcpPath = path16.join(dir, ".cursor", "mcp.json");
2291
- if (fs18.existsSync(cursorMcpPath)) {
2392
+ const cursorMcpPath = path17.join(dir, ".cursor", "mcp.json");
2393
+ if (fs19.existsSync(cursorMcpPath)) {
2292
2394
  try {
2293
- const mcpJson = JSON.parse(fs18.readFileSync(cursorMcpPath, "utf-8"));
2395
+ const mcpJson = JSON.parse(fs19.readFileSync(cursorMcpPath, "utf-8"));
2294
2396
  if (mcpJson.mcpServers) {
2295
2397
  for (const name of Object.keys(mcpJson.mcpServers)) {
2296
2398
  items.push({
@@ -2334,7 +2436,7 @@ function compareState(serverItems, localItems) {
2334
2436
  return { installed, missing, outdated, extra };
2335
2437
  }
2336
2438
  function hashFile(filePath) {
2337
- const text = fs18.readFileSync(filePath, "utf-8");
2439
+ const text = fs19.readFileSync(filePath, "utf-8");
2338
2440
  return crypto4.createHash("sha256").update(JSON.stringify({ text })).digest("hex");
2339
2441
  }
2340
2442
  function hashJson(obj) {
@@ -2366,7 +2468,7 @@ async function statusCommand(options) {
2366
2468
  }
2367
2469
  console.log(` Files managed: ${chalk5.cyan(manifest.entries.length.toString())}`);
2368
2470
  for (const entry of manifest.entries) {
2369
- const exists = fs19.existsSync(entry.path);
2471
+ const exists = fs20.existsSync(entry.path);
2370
2472
  const icon = exists ? chalk5.green("\u2713") : chalk5.red("\u2717");
2371
2473
  console.log(` ${icon} ${entry.path} (${entry.action})`);
2372
2474
  }
@@ -3001,8 +3103,8 @@ async function diffCommand(options) {
3001
3103
  }
3002
3104
 
3003
3105
  // src/commands/refresh.ts
3004
- import fs21 from "fs";
3005
- import path18 from "path";
3106
+ import fs22 from "fs";
3107
+ import path19 from "path";
3006
3108
  import chalk11 from "chalk";
3007
3109
  import ora8 from "ora";
3008
3110
 
@@ -3079,37 +3181,37 @@ function collectDiff(lastSha) {
3079
3181
  }
3080
3182
 
3081
3183
  // src/writers/refresh.ts
3082
- import fs20 from "fs";
3083
- import path17 from "path";
3184
+ import fs21 from "fs";
3185
+ import path18 from "path";
3084
3186
  function writeRefreshDocs(docs) {
3085
3187
  const written = [];
3086
3188
  if (docs.claudeMd) {
3087
- fs20.writeFileSync("CLAUDE.md", docs.claudeMd);
3189
+ fs21.writeFileSync("CLAUDE.md", docs.claudeMd);
3088
3190
  written.push("CLAUDE.md");
3089
3191
  }
3090
3192
  if (docs.readmeMd) {
3091
- fs20.writeFileSync("README.md", docs.readmeMd);
3193
+ fs21.writeFileSync("README.md", docs.readmeMd);
3092
3194
  written.push("README.md");
3093
3195
  }
3094
3196
  if (docs.cursorrules) {
3095
- fs20.writeFileSync(".cursorrules", docs.cursorrules);
3197
+ fs21.writeFileSync(".cursorrules", docs.cursorrules);
3096
3198
  written.push(".cursorrules");
3097
3199
  }
3098
3200
  if (docs.cursorRules) {
3099
- const rulesDir = path17.join(".cursor", "rules");
3100
- if (!fs20.existsSync(rulesDir)) fs20.mkdirSync(rulesDir, { recursive: true });
3201
+ const rulesDir = path18.join(".cursor", "rules");
3202
+ if (!fs21.existsSync(rulesDir)) fs21.mkdirSync(rulesDir, { recursive: true });
3101
3203
  for (const rule of docs.cursorRules) {
3102
- const filePath = path17.join(rulesDir, rule.filename);
3103
- fs20.writeFileSync(filePath, rule.content);
3204
+ const filePath = path18.join(rulesDir, rule.filename);
3205
+ fs21.writeFileSync(filePath, rule.content);
3104
3206
  written.push(filePath);
3105
3207
  }
3106
3208
  }
3107
3209
  if (docs.claudeSkills) {
3108
- const skillsDir = path17.join(".claude", "skills");
3109
- if (!fs20.existsSync(skillsDir)) fs20.mkdirSync(skillsDir, { recursive: true });
3210
+ const skillsDir = path18.join(".claude", "skills");
3211
+ if (!fs21.existsSync(skillsDir)) fs21.mkdirSync(skillsDir, { recursive: true });
3110
3212
  for (const skill of docs.claudeSkills) {
3111
- const filePath = path17.join(skillsDir, skill.filename);
3112
- fs20.writeFileSync(filePath, skill.content);
3213
+ const filePath = path18.join(skillsDir, skill.filename);
3214
+ fs21.writeFileSync(filePath, skill.content);
3113
3215
  written.push(filePath);
3114
3216
  }
3115
3217
  }
@@ -3123,11 +3225,11 @@ function log(quiet, ...args) {
3123
3225
  function discoverGitRepos(parentDir) {
3124
3226
  const repos = [];
3125
3227
  try {
3126
- const entries = fs21.readdirSync(parentDir, { withFileTypes: true });
3228
+ const entries = fs22.readdirSync(parentDir, { withFileTypes: true });
3127
3229
  for (const entry of entries) {
3128
3230
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
3129
- const childPath = path18.join(parentDir, entry.name);
3130
- if (fs21.existsSync(path18.join(childPath, ".git"))) {
3231
+ const childPath = path19.join(parentDir, entry.name);
3232
+ if (fs22.existsSync(path19.join(childPath, ".git"))) {
3131
3233
  repos.push(childPath);
3132
3234
  }
3133
3235
  }
@@ -3230,7 +3332,7 @@ async function refreshCommand(options) {
3230
3332
  `));
3231
3333
  const originalDir = process.cwd();
3232
3334
  for (const repo of repos) {
3233
- const repoName = path18.basename(repo);
3335
+ const repoName = path19.basename(repo);
3234
3336
  try {
3235
3337
  process.chdir(repo);
3236
3338
  await refreshSingleRepo(repo, { ...options, label: repoName });
@@ -3371,10 +3473,287 @@ async function reviewCommand(message, options) {
3371
3473
  await submitReview({ rating, bestPart, biggestGap, wouldRecommend });
3372
3474
  }
3373
3475
 
3476
+ // src/commands/learn.ts
3477
+ import chalk14 from "chalk";
3478
+
3479
+ // src/learner/stdin.ts
3480
+ var STDIN_TIMEOUT_MS = 5e3;
3481
+ function readStdin() {
3482
+ return new Promise((resolve2, reject) => {
3483
+ if (process.stdin.isTTY) {
3484
+ resolve2("");
3485
+ return;
3486
+ }
3487
+ const chunks = [];
3488
+ const timer = setTimeout(() => {
3489
+ process.stdin.removeAllListeners();
3490
+ process.stdin.destroy();
3491
+ resolve2(Buffer.concat(chunks).toString("utf-8"));
3492
+ }, STDIN_TIMEOUT_MS);
3493
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
3494
+ process.stdin.on("end", () => {
3495
+ clearTimeout(timer);
3496
+ resolve2(Buffer.concat(chunks).toString("utf-8"));
3497
+ });
3498
+ process.stdin.on("error", (err) => {
3499
+ clearTimeout(timer);
3500
+ reject(err);
3501
+ });
3502
+ process.stdin.resume();
3503
+ });
3504
+ }
3505
+
3506
+ // src/learner/storage.ts
3507
+ init_constants();
3508
+ import fs23 from "fs";
3509
+ import path20 from "path";
3510
+ var MAX_RESPONSE_LENGTH = 2e3;
3511
+ var DEFAULT_STATE = {
3512
+ sessionId: null,
3513
+ eventCount: 0,
3514
+ lastAnalysisTimestamp: null
3515
+ };
3516
+ function ensureLearningDir() {
3517
+ if (!fs23.existsSync(LEARNING_DIR)) {
3518
+ fs23.mkdirSync(LEARNING_DIR, { recursive: true });
3519
+ }
3520
+ }
3521
+ function sessionFilePath() {
3522
+ return path20.join(LEARNING_DIR, LEARNING_SESSION_FILE);
3523
+ }
3524
+ function stateFilePath() {
3525
+ return path20.join(LEARNING_DIR, LEARNING_STATE_FILE);
3526
+ }
3527
+ function truncateResponse(response) {
3528
+ const str = JSON.stringify(response);
3529
+ if (str.length <= MAX_RESPONSE_LENGTH) return response;
3530
+ return { _truncated: str.slice(0, MAX_RESPONSE_LENGTH) };
3531
+ }
3532
+ function appendEvent(event) {
3533
+ ensureLearningDir();
3534
+ const truncated = { ...event, tool_response: truncateResponse(event.tool_response) };
3535
+ const filePath = sessionFilePath();
3536
+ fs23.appendFileSync(filePath, JSON.stringify(truncated) + "\n");
3537
+ const count = getEventCount();
3538
+ if (count > LEARNING_MAX_EVENTS) {
3539
+ const lines = fs23.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
3540
+ const kept = lines.slice(lines.length - LEARNING_MAX_EVENTS);
3541
+ fs23.writeFileSync(filePath, kept.join("\n") + "\n");
3542
+ }
3543
+ }
3544
+ function readAllEvents() {
3545
+ const filePath = sessionFilePath();
3546
+ if (!fs23.existsSync(filePath)) return [];
3547
+ const lines = fs23.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
3548
+ return lines.map((line) => JSON.parse(line));
3549
+ }
3550
+ function getEventCount() {
3551
+ const filePath = sessionFilePath();
3552
+ if (!fs23.existsSync(filePath)) return 0;
3553
+ const content = fs23.readFileSync(filePath, "utf-8");
3554
+ return content.split("\n").filter(Boolean).length;
3555
+ }
3556
+ function clearSession() {
3557
+ const filePath = sessionFilePath();
3558
+ if (fs23.existsSync(filePath)) fs23.unlinkSync(filePath);
3559
+ }
3560
+ function readState2() {
3561
+ const filePath = stateFilePath();
3562
+ if (!fs23.existsSync(filePath)) return { ...DEFAULT_STATE };
3563
+ try {
3564
+ return JSON.parse(fs23.readFileSync(filePath, "utf-8"));
3565
+ } catch {
3566
+ return { ...DEFAULT_STATE };
3567
+ }
3568
+ }
3569
+ function writeState2(state) {
3570
+ ensureLearningDir();
3571
+ fs23.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
3572
+ }
3573
+ function resetState() {
3574
+ writeState2({ ...DEFAULT_STATE });
3575
+ }
3576
+
3577
+ // src/learner/writer.ts
3578
+ import fs24 from "fs";
3579
+ import path21 from "path";
3580
+ var LEARNED_START = "<!-- caliber:learned -->";
3581
+ var LEARNED_END = "<!-- /caliber:learned -->";
3582
+ function writeLearnedContent(update) {
3583
+ const written = [];
3584
+ if (update.claudeMdLearnedSection) {
3585
+ writeLearnedSection(update.claudeMdLearnedSection);
3586
+ written.push("CLAUDE.md");
3587
+ }
3588
+ if (update.skills?.length) {
3589
+ for (const skill of update.skills) {
3590
+ const skillPath = writeLearnedSkill(skill);
3591
+ written.push(skillPath);
3592
+ }
3593
+ }
3594
+ return written;
3595
+ }
3596
+ function writeLearnedSection(content) {
3597
+ const claudeMdPath = "CLAUDE.md";
3598
+ let existing = "";
3599
+ if (fs24.existsSync(claudeMdPath)) {
3600
+ existing = fs24.readFileSync(claudeMdPath, "utf-8");
3601
+ }
3602
+ const section = `${LEARNED_START}
3603
+ ${content}
3604
+ ${LEARNED_END}`;
3605
+ const startIdx = existing.indexOf(LEARNED_START);
3606
+ const endIdx = existing.indexOf(LEARNED_END);
3607
+ let updated;
3608
+ if (startIdx !== -1 && endIdx !== -1) {
3609
+ updated = existing.slice(0, startIdx) + section + existing.slice(endIdx + LEARNED_END.length);
3610
+ } else {
3611
+ const separator = existing.endsWith("\n") || existing === "" ? "" : "\n";
3612
+ updated = existing + separator + "\n" + section + "\n";
3613
+ }
3614
+ fs24.writeFileSync(claudeMdPath, updated);
3615
+ }
3616
+ function writeLearnedSkill(skill) {
3617
+ const skillDir = path21.join(".claude", "skills", skill.name);
3618
+ if (!fs24.existsSync(skillDir)) fs24.mkdirSync(skillDir, { recursive: true });
3619
+ const skillPath = path21.join(skillDir, "SKILL.md");
3620
+ if (!skill.isNew && fs24.existsSync(skillPath)) {
3621
+ const existing = fs24.readFileSync(skillPath, "utf-8");
3622
+ fs24.writeFileSync(skillPath, existing.trimEnd() + "\n\n" + skill.content);
3623
+ } else {
3624
+ const frontmatter = [
3625
+ "---",
3626
+ `name: ${skill.name}`,
3627
+ `description: ${skill.description}`,
3628
+ "---",
3629
+ ""
3630
+ ].join("\n");
3631
+ fs24.writeFileSync(skillPath, frontmatter + skill.content);
3632
+ }
3633
+ return skillPath;
3634
+ }
3635
+ function readLearnedSection() {
3636
+ const claudeMdPath = "CLAUDE.md";
3637
+ if (!fs24.existsSync(claudeMdPath)) return null;
3638
+ const content = fs24.readFileSync(claudeMdPath, "utf-8");
3639
+ const startIdx = content.indexOf(LEARNED_START);
3640
+ const endIdx = content.indexOf(LEARNED_END);
3641
+ if (startIdx === -1 || endIdx === -1) return null;
3642
+ return content.slice(startIdx + LEARNED_START.length, endIdx).trim();
3643
+ }
3644
+
3645
+ // src/commands/learn.ts
3646
+ async function learnObserveCommand(options) {
3647
+ try {
3648
+ const raw = await readStdin();
3649
+ if (!raw.trim()) return;
3650
+ const hookData = JSON.parse(raw);
3651
+ const event = {
3652
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3653
+ session_id: hookData.session_id || "unknown",
3654
+ hook_event_name: options.failure ? "PostToolUseFailure" : "PostToolUse",
3655
+ tool_name: hookData.tool_name || "unknown",
3656
+ tool_input: hookData.tool_input || {},
3657
+ tool_response: hookData.tool_response || {},
3658
+ tool_use_id: hookData.tool_use_id || "",
3659
+ cwd: hookData.cwd || process.cwd()
3660
+ };
3661
+ appendEvent(event);
3662
+ const state = readState2();
3663
+ state.eventCount++;
3664
+ if (!state.sessionId) state.sessionId = event.session_id;
3665
+ writeState2(state);
3666
+ } catch {
3667
+ }
3668
+ }
3669
+ async function learnFinalizeCommand() {
3670
+ try {
3671
+ const auth2 = getStoredAuth();
3672
+ if (!auth2) {
3673
+ clearSession();
3674
+ resetState();
3675
+ return;
3676
+ }
3677
+ const events = readAllEvents();
3678
+ if (!events.length) {
3679
+ clearSession();
3680
+ resetState();
3681
+ return;
3682
+ }
3683
+ const existingConfigs = readExistingConfigs(process.cwd());
3684
+ const existingLearnedSection = readLearnedSection();
3685
+ const existingSkills = existingConfigs.claudeSkills || [];
3686
+ const response = await apiRequest("/api/learn/analyze", {
3687
+ method: "POST",
3688
+ body: {
3689
+ events,
3690
+ existingClaudeMd: existingConfigs.claudeMd || "",
3691
+ existingLearnedSection,
3692
+ existingSkills
3693
+ }
3694
+ });
3695
+ if (response.claudeMdLearnedSection || response.skills?.length) {
3696
+ writeLearnedContent({
3697
+ claudeMdLearnedSection: response.claudeMdLearnedSection,
3698
+ skills: response.skills
3699
+ });
3700
+ }
3701
+ } catch {
3702
+ } finally {
3703
+ clearSession();
3704
+ resetState();
3705
+ }
3706
+ }
3707
+ async function learnInstallCommand() {
3708
+ const result = installLearningHooks();
3709
+ if (result.alreadyInstalled) {
3710
+ console.log(chalk14.dim("Learning hooks already installed."));
3711
+ return;
3712
+ }
3713
+ console.log(chalk14.green("\u2713") + " Learning hooks installed in .claude/settings.json");
3714
+ console.log(chalk14.dim(" PostToolUse, PostToolUseFailure, and SessionEnd hooks active."));
3715
+ console.log(chalk14.dim(" Session learnings will be written to CLAUDE.md and skills."));
3716
+ }
3717
+ async function learnRemoveCommand() {
3718
+ const result = removeLearningHooks();
3719
+ if (result.notFound) {
3720
+ console.log(chalk14.dim("Learning hooks not found."));
3721
+ return;
3722
+ }
3723
+ console.log(chalk14.green("\u2713") + " Learning hooks removed from .claude/settings.json");
3724
+ }
3725
+ async function learnStatusCommand() {
3726
+ const installed = areLearningHooksInstalled();
3727
+ const state = readState2();
3728
+ const eventCount = getEventCount();
3729
+ console.log(chalk14.bold("Session Learning Status"));
3730
+ console.log();
3731
+ if (installed) {
3732
+ console.log(chalk14.green("\u2713") + " Learning hooks are " + chalk14.green("installed"));
3733
+ } else {
3734
+ console.log(chalk14.dim("\u2717") + " Learning hooks are " + chalk14.yellow("not installed"));
3735
+ console.log(chalk14.dim(" Run `caliber learn install` to enable session learning."));
3736
+ }
3737
+ console.log();
3738
+ console.log(`Events recorded: ${chalk14.cyan(String(eventCount))}`);
3739
+ console.log(`Total this session: ${chalk14.cyan(String(state.eventCount))}`);
3740
+ if (state.lastAnalysisTimestamp) {
3741
+ console.log(`Last analysis: ${chalk14.cyan(state.lastAnalysisTimestamp)}`);
3742
+ } else {
3743
+ console.log(`Last analysis: ${chalk14.dim("none")}`);
3744
+ }
3745
+ const learnedSection = readLearnedSection();
3746
+ if (learnedSection) {
3747
+ const lineCount = learnedSection.split("\n").filter(Boolean).length;
3748
+ console.log(`
3749
+ Learned items in CLAUDE.md: ${chalk14.cyan(String(lineCount))}`);
3750
+ }
3751
+ }
3752
+
3374
3753
  // src/cli.ts
3375
- var __dirname2 = path19.dirname(fileURLToPath3(import.meta.url));
3754
+ var __dirname2 = path22.dirname(fileURLToPath3(import.meta.url));
3376
3755
  var pkg3 = JSON.parse(
3377
- fs22.readFileSync(path19.resolve(__dirname2, "..", "package.json"), "utf-8")
3756
+ fs25.readFileSync(path22.resolve(__dirname2, "..", "package.json"), "utf-8")
3378
3757
  );
3379
3758
  var program = new Command();
3380
3759
  var displayVersion = process.env.CALIBER_LOCAL ? `${pkg3.version}-local` : pkg3.version;
@@ -3394,24 +3773,30 @@ var hooks = program.command("hooks").description("Manage Claude Code session hoo
3394
3773
  hooks.command("install").description("Install auto-refresh SessionEnd hook").action(hooksInstallCommand);
3395
3774
  hooks.command("remove").description("Remove auto-refresh SessionEnd hook").action(hooksRemoveCommand);
3396
3775
  hooks.command("status").description("Check if auto-refresh hook is installed").action(hooksStatusCommand);
3776
+ var learn = program.command("learn").description("Session learning \u2014 observe tool usage and extract reusable instructions");
3777
+ learn.command("observe").description("Record a tool event from stdin (called by hooks)").option("--failure", "Mark event as a tool failure").action(learnObserveCommand);
3778
+ learn.command("finalize").description("Analyze session events and update CLAUDE.md (called on SessionEnd)").action(learnFinalizeCommand);
3779
+ learn.command("install").description("Install learning hooks into .claude/settings.json").action(learnInstallCommand);
3780
+ learn.command("remove").description("Remove learning hooks from .claude/settings.json").action(learnRemoveCommand);
3781
+ learn.command("status").description("Show learning system status").action(learnStatusCommand);
3397
3782
 
3398
3783
  // src/utils/version-check.ts
3399
- import fs23 from "fs";
3400
- import path20 from "path";
3784
+ import fs26 from "fs";
3785
+ import path23 from "path";
3401
3786
  import { fileURLToPath as fileURLToPath4 } from "url";
3402
3787
  import { execSync as execSync5 } from "child_process";
3403
- import chalk14 from "chalk";
3788
+ import chalk15 from "chalk";
3404
3789
  import ora10 from "ora";
3405
3790
  import confirm3 from "@inquirer/confirm";
3406
- var __dirname_vc = path20.dirname(fileURLToPath4(import.meta.url));
3791
+ var __dirname_vc = path23.dirname(fileURLToPath4(import.meta.url));
3407
3792
  var pkg4 = JSON.parse(
3408
- fs23.readFileSync(path20.resolve(__dirname_vc, "..", "package.json"), "utf-8")
3793
+ fs26.readFileSync(path23.resolve(__dirname_vc, "..", "package.json"), "utf-8")
3409
3794
  );
3410
3795
  function getInstalledVersion() {
3411
3796
  try {
3412
3797
  const globalRoot = execSync5("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3413
- const pkgPath = path20.join(globalRoot, "@caliber-ai", "cli", "package.json");
3414
- return JSON.parse(fs23.readFileSync(pkgPath, "utf-8")).version;
3798
+ const pkgPath = path23.join(globalRoot, "@caliber-ai", "cli", "package.json");
3799
+ return JSON.parse(fs26.readFileSync(pkgPath, "utf-8")).version;
3415
3800
  } catch {
3416
3801
  return null;
3417
3802
  }
@@ -3434,17 +3819,17 @@ async function checkForUpdates() {
3434
3819
  const isInteractive = process.stdin.isTTY === true;
3435
3820
  if (!isInteractive) {
3436
3821
  console.log(
3437
- chalk14.yellow(
3822
+ chalk15.yellow(
3438
3823
  `
3439
3824
  Update available: ${current} -> ${latest}
3440
- Run ${chalk14.bold("npm install -g @caliber-ai/cli")} to upgrade.
3825
+ Run ${chalk15.bold("npm install -g @caliber-ai/cli")} to upgrade.
3441
3826
  `
3442
3827
  )
3443
3828
  );
3444
3829
  return;
3445
3830
  }
3446
3831
  console.log(
3447
- chalk14.yellow(`
3832
+ chalk15.yellow(`
3448
3833
  Update available: ${current} -> ${latest}`)
3449
3834
  );
3450
3835
  const shouldUpdate = await confirm3({ message: "Would you like to update now? (Y/n)", default: true });
@@ -3458,13 +3843,13 @@ Update available: ${current} -> ${latest}`)
3458
3843
  const installed = getInstalledVersion();
3459
3844
  if (installed !== latest) {
3460
3845
  spinner.fail(`Update incomplete \u2014 got ${installed ?? "unknown"}, expected ${latest}`);
3461
- console.log(chalk14.yellow(`Run ${chalk14.bold(`npm install -g @caliber-ai/cli@${latest}`)} manually.
3846
+ console.log(chalk15.yellow(`Run ${chalk15.bold(`npm install -g @caliber-ai/cli@${latest}`)} manually.
3462
3847
  `));
3463
3848
  return;
3464
3849
  }
3465
- spinner.succeed(chalk14.green(`Updated to ${latest}`));
3850
+ spinner.succeed(chalk15.green(`Updated to ${latest}`));
3466
3851
  const args = process.argv.slice(2);
3467
- console.log(chalk14.dim(`
3852
+ console.log(chalk15.dim(`
3468
3853
  Restarting: caliber ${args.join(" ")}
3469
3854
  `));
3470
3855
  execSync5(`caliber ${args.map((a) => JSON.stringify(a)).join(" ")}`, {
@@ -3475,10 +3860,10 @@ Restarting: caliber ${args.join(" ")}
3475
3860
  } catch (err) {
3476
3861
  spinner.fail("Update failed");
3477
3862
  const msg = err instanceof Error ? err.message : "";
3478
- if (msg && !msg.includes("SIGTERM")) console.log(chalk14.dim(` ${msg.split("\n")[0]}`));
3863
+ if (msg && !msg.includes("SIGTERM")) console.log(chalk15.dim(` ${msg.split("\n")[0]}`));
3479
3864
  console.log(
3480
- chalk14.yellow(
3481
- `Run ${chalk14.bold(`npm install -g @caliber-ai/cli@${latest}`)} manually to upgrade.
3865
+ chalk15.yellow(
3866
+ `Run ${chalk15.bold(`npm install -g @caliber-ai/cli@${latest}`)} manually to upgrade.
3482
3867
  `
3483
3868
  )
3484
3869
  );