@caliber-ai/cli 0.21.1 → 0.23.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
@@ -583,11 +591,18 @@ function readExistingConfigs(dir) {
583
591
  const skillsDir = path7.join(dir, ".claude", "skills");
584
592
  if (fs6.existsSync(skillsDir)) {
585
593
  try {
586
- const files = fs6.readdirSync(skillsDir).filter((f) => f.endsWith(".md"));
587
- configs.claudeSkills = files.map((f) => ({
588
- filename: f,
589
- content: fs6.readFileSync(path7.join(skillsDir, f), "utf-8")
590
- }));
594
+ const entries = fs6.readdirSync(skillsDir);
595
+ const skills = [];
596
+ for (const entry of entries) {
597
+ const entryPath = path7.join(skillsDir, entry);
598
+ const skillMdPath = path7.join(entryPath, "SKILL.md");
599
+ if (fs6.statSync(entryPath).isDirectory() && fs6.existsSync(skillMdPath)) {
600
+ skills.push({ filename: `${entry}/SKILL.md`, content: fs6.readFileSync(skillMdPath, "utf-8") });
601
+ } else if (entry.endsWith(".md")) {
602
+ skills.push({ filename: entry, content: fs6.readFileSync(entryPath, "utf-8") });
603
+ }
604
+ }
605
+ if (skills.length > 0) configs.claudeSkills = skills;
591
606
  } catch {
592
607
  }
593
608
  }
@@ -612,10 +627,10 @@ function readExistingConfigs(dir) {
612
627
  const slugs = fs6.readdirSync(cursorSkillsDir).filter((f) => {
613
628
  return fs6.statSync(path7.join(cursorSkillsDir, f)).isDirectory();
614
629
  });
615
- configs.cursorSkills = slugs.filter((slug) => fs6.existsSync(path7.join(cursorSkillsDir, slug, "SKILL.md"))).map((slug) => ({
616
- slug,
630
+ configs.cursorSkills = slugs.filter((slug) => fs6.existsSync(path7.join(cursorSkillsDir, slug, "SKILL.md"))).map((name) => ({
631
+ name,
617
632
  filename: "SKILL.md",
618
- content: fs6.readFileSync(path7.join(cursorSkillsDir, slug, "SKILL.md"), "utf-8")
633
+ content: fs6.readFileSync(path7.join(cursorSkillsDir, name, "SKILL.md"), "utf-8")
619
634
  }));
620
635
  } catch {
621
636
  }
@@ -950,9 +965,9 @@ async function getValidToken() {
950
965
  }
951
966
  return refreshed;
952
967
  }
953
- async function apiRequest(path21, options = {}) {
968
+ async function apiRequest(path24, options = {}) {
954
969
  let token = await getValidToken();
955
- let resp = await fetch(`${API_URL}${path21}`, {
970
+ let resp = await fetch(`${API_URL}${path24}`, {
956
971
  method: options.method || "GET",
957
972
  headers: {
958
973
  "Content-Type": "application/json",
@@ -966,7 +981,7 @@ async function apiRequest(path21, options = {}) {
966
981
  throw new Error("Session expired. Run `caliber login` to re-authenticate.");
967
982
  }
968
983
  token = refreshed;
969
- resp = await fetch(`${API_URL}${path21}`, {
984
+ resp = await fetch(`${API_URL}${path24}`, {
970
985
  method: options.method || "GET",
971
986
  headers: {
972
987
  "Content-Type": "application/json",
@@ -982,9 +997,9 @@ async function apiRequest(path21, options = {}) {
982
997
  const json = await resp.json();
983
998
  return json.data;
984
999
  }
985
- async function apiStream(path21, body, onChunk, onComplete, onError, onStatus) {
1000
+ async function apiStream(path24, body, onChunk, onComplete, onError, onStatus) {
986
1001
  let token = await getValidToken();
987
- let resp = await fetch(`${API_URL}${path21}`, {
1002
+ let resp = await fetch(`${API_URL}${path24}`, {
988
1003
  method: "POST",
989
1004
  headers: {
990
1005
  "Content-Type": "application/json",
@@ -998,7 +1013,7 @@ async function apiStream(path21, body, onChunk, onComplete, onError, onStatus) {
998
1013
  throw new Error("Session expired. Run `caliber login` to re-authenticate.");
999
1014
  }
1000
1015
  token = refreshed;
1001
- resp = await fetch(`${API_URL}${path21}`, {
1016
+ resp = await fetch(`${API_URL}${path24}`, {
1002
1017
  method: "POST",
1003
1018
  headers: {
1004
1019
  "Content-Type": "application/json",
@@ -1126,12 +1141,18 @@ function writeClaudeConfig(config) {
1126
1141
  fs9.writeFileSync("CLAUDE.md", config.claudeMd);
1127
1142
  written.push("CLAUDE.md");
1128
1143
  if (config.skills?.length) {
1129
- const skillsDir = path10.join(".claude", "skills");
1130
- if (!fs9.existsSync(skillsDir)) fs9.mkdirSync(skillsDir, { recursive: true });
1131
1144
  for (const skill of config.skills) {
1132
- const filename = `${skill.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}.md`;
1133
- const skillPath = path10.join(skillsDir, filename);
1134
- fs9.writeFileSync(skillPath, skill.content);
1145
+ const skillDir = path10.join(".claude", "skills", skill.name);
1146
+ if (!fs9.existsSync(skillDir)) fs9.mkdirSync(skillDir, { recursive: true });
1147
+ const skillPath = path10.join(skillDir, "SKILL.md");
1148
+ const frontmatter = [
1149
+ "---",
1150
+ `name: ${skill.name}`,
1151
+ `description: ${skill.description}`,
1152
+ "---",
1153
+ ""
1154
+ ].join("\n");
1155
+ fs9.writeFileSync(skillPath, frontmatter + skill.content);
1135
1156
  written.push(skillPath);
1136
1157
  }
1137
1158
  }
@@ -1171,10 +1192,17 @@ function writeCursorConfig(config) {
1171
1192
  }
1172
1193
  if (config.skills?.length) {
1173
1194
  for (const skill of config.skills) {
1174
- const skillDir = path11.join(".cursor", "skills", skill.slug);
1195
+ const skillDir = path11.join(".cursor", "skills", skill.name);
1175
1196
  if (!fs10.existsSync(skillDir)) fs10.mkdirSync(skillDir, { recursive: true });
1176
1197
  const skillPath = path11.join(skillDir, "SKILL.md");
1177
- fs10.writeFileSync(skillPath, skill.content);
1198
+ const frontmatter = [
1199
+ "---",
1200
+ `name: ${skill.name}`,
1201
+ `description: ${skill.description}`,
1202
+ "---",
1203
+ ""
1204
+ ].join("\n");
1205
+ fs10.writeFileSync(skillPath, frontmatter + skill.content);
1178
1206
  written.push(skillPath);
1179
1207
  }
1180
1208
  }
@@ -1186,9 +1214,7 @@ function writeCursorConfig(config) {
1186
1214
  try {
1187
1215
  if (fs10.existsSync(mcpPath)) {
1188
1216
  const existing = JSON.parse(fs10.readFileSync(mcpPath, "utf-8"));
1189
- if (existing.mcpServers) {
1190
- existingServers = existing.mcpServers;
1191
- }
1217
+ if (existing.mcpServers) existingServers = existing.mcpServers;
1192
1218
  }
1193
1219
  } catch {
1194
1220
  }
@@ -1681,7 +1707,7 @@ async function initCommand(options) {
1681
1707
  }
1682
1708
  if (Array.isArray(ec.cursorSkills)) {
1683
1709
  for (const skill of ec.cursorSkills) {
1684
- localConfigs.push(`.cursor/skills/${skill.slug}/SKILL.md`);
1710
+ localConfigs.push(`.cursor/skills/${skill.name}/SKILL.md`);
1685
1711
  }
1686
1712
  }
1687
1713
  let existingSetup = null;
@@ -2050,11 +2076,11 @@ function printSetupSummary(setup) {
2050
2076
  const skills = claude.skills;
2051
2077
  if (Array.isArray(skills) && skills.length > 0) {
2052
2078
  for (const skill of skills) {
2053
- const skillPath = `.claude/skills/${skill.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}.md`;
2079
+ const skillPath = `.claude/skills/${skill.name}/SKILL.md`;
2054
2080
  const icon = fs17.existsSync(skillPath) ? chalk3.yellow("~") : chalk3.green("+");
2055
2081
  const desc = getDescription(skillPath);
2056
2082
  console.log(` ${icon} ${chalk3.bold(skillPath)}`);
2057
- console.log(chalk3.dim(` ${desc || summarizeSkill(skill)}`));
2083
+ console.log(chalk3.dim(` ${desc || skill.description || skill.name}`));
2058
2084
  console.log("");
2059
2085
  }
2060
2086
  }
@@ -2070,16 +2096,11 @@ function printSetupSummary(setup) {
2070
2096
  const cursorSkills = cursor.skills;
2071
2097
  if (Array.isArray(cursorSkills) && cursorSkills.length > 0) {
2072
2098
  for (const skill of cursorSkills) {
2073
- const skillPath = `.cursor/skills/${skill.slug}/SKILL.md`;
2099
+ const skillPath = `.cursor/skills/${skill.name}/SKILL.md`;
2074
2100
  const icon = fs17.existsSync(skillPath) ? chalk3.yellow("~") : chalk3.green("+");
2075
2101
  const desc = getDescription(skillPath);
2076
2102
  console.log(` ${icon} ${chalk3.bold(skillPath)}`);
2077
- if (desc) {
2078
- console.log(chalk3.dim(` ${desc}`));
2079
- } else {
2080
- const firstLine = skill.content.split("\n").filter((l) => l.trim() && !l.trim().startsWith("#") && !l.trim().startsWith("---"))[0];
2081
- if (firstLine) console.log(chalk3.dim(` ${firstLine.trim().slice(0, 80)}`));
2082
- }
2103
+ console.log(chalk3.dim(` ${desc || skill.description || skill.name}`));
2083
2104
  console.log("");
2084
2105
  }
2085
2106
  }
@@ -2110,9 +2131,14 @@ function printSetupSummary(setup) {
2110
2131
  console.log(` ${chalk3.green("+")} ${chalk3.dim("new")} ${chalk3.yellow("~")} ${chalk3.dim("modified")} ${chalk3.red("-")} ${chalk3.dim("removed")}`);
2111
2132
  console.log("");
2112
2133
  }
2113
- function summarizeSkill(skill) {
2114
- const lines = skill.content.split("\n").filter((l) => l.trim() && !l.trim().startsWith("#"));
2115
- return lines[0]?.trim().slice(0, 80) || skill.name;
2134
+ function buildSkillContent(skill) {
2135
+ const frontmatter = `---
2136
+ name: ${skill.name}
2137
+ description: ${skill.description}
2138
+ ---
2139
+
2140
+ `;
2141
+ return frontmatter + skill.content;
2116
2142
  }
2117
2143
  function collectSetupFiles(setup) {
2118
2144
  const files = [];
@@ -2123,8 +2149,7 @@ function collectSetupFiles(setup) {
2123
2149
  const skills = claude.skills;
2124
2150
  if (Array.isArray(skills)) {
2125
2151
  for (const skill of skills) {
2126
- const skillPath = `.claude/skills/${skill.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}.md`;
2127
- files.push({ path: skillPath, content: skill.content });
2152
+ files.push({ path: `.claude/skills/${skill.name}/SKILL.md`, content: buildSkillContent(skill) });
2128
2153
  }
2129
2154
  }
2130
2155
  }
@@ -2133,7 +2158,7 @@ function collectSetupFiles(setup) {
2133
2158
  const cursorSkills = cursor.skills;
2134
2159
  if (Array.isArray(cursorSkills)) {
2135
2160
  for (const skill of cursorSkills) {
2136
- files.push({ path: `.cursor/skills/${skill.slug}/SKILL.md`, content: skill.content });
2161
+ files.push({ path: `.cursor/skills/${skill.name}/SKILL.md`, content: buildSkillContent(skill) });
2137
2162
  }
2138
2163
  }
2139
2164
  const rules = cursor.rules;
@@ -2255,13 +2280,13 @@ function scanLocalState(dir) {
2255
2280
  const cursorSkillsDir = path16.join(dir, ".cursor", "skills");
2256
2281
  if (fs18.existsSync(cursorSkillsDir)) {
2257
2282
  try {
2258
- for (const slug of fs18.readdirSync(cursorSkillsDir)) {
2259
- const skillFile = path16.join(cursorSkillsDir, slug, "SKILL.md");
2283
+ for (const name of fs18.readdirSync(cursorSkillsDir)) {
2284
+ const skillFile = path16.join(cursorSkillsDir, name, "SKILL.md");
2260
2285
  if (fs18.existsSync(skillFile)) {
2261
2286
  items.push({
2262
2287
  type: "skill",
2263
2288
  platform: "cursor",
2264
- name: `${slug}/SKILL.md`,
2289
+ name: `${name}/SKILL.md`,
2265
2290
  contentHash: hashFile(skillFile),
2266
2291
  path: skillFile
2267
2292
  });
@@ -2499,7 +2524,7 @@ function getSkillPath(platform, slug) {
2499
2524
  if (platform === "cursor") {
2500
2525
  return join(".cursor", "skills", slug, "SKILL.md");
2501
2526
  }
2502
- return join(".claude", "skills", `${slug}.md`);
2527
+ return join(".claude", "skills", slug, "SKILL.md");
2503
2528
  }
2504
2529
  async function recommendCommand(options) {
2505
2530
  const auth2 = getStoredAuth();
@@ -3354,10 +3379,374 @@ async function reviewCommand(message, options) {
3354
3379
  await submitReview({ rating, bestPart, biggestGap, wouldRecommend });
3355
3380
  }
3356
3381
 
3382
+ // src/commands/learn.ts
3383
+ import chalk14 from "chalk";
3384
+
3385
+ // src/learner/stdin.ts
3386
+ var STDIN_TIMEOUT_MS = 5e3;
3387
+ function readStdin() {
3388
+ return new Promise((resolve2, reject) => {
3389
+ if (process.stdin.isTTY) {
3390
+ resolve2("");
3391
+ return;
3392
+ }
3393
+ const chunks = [];
3394
+ const timer = setTimeout(() => {
3395
+ process.stdin.removeAllListeners();
3396
+ process.stdin.destroy();
3397
+ resolve2(Buffer.concat(chunks).toString("utf-8"));
3398
+ }, STDIN_TIMEOUT_MS);
3399
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
3400
+ process.stdin.on("end", () => {
3401
+ clearTimeout(timer);
3402
+ resolve2(Buffer.concat(chunks).toString("utf-8"));
3403
+ });
3404
+ process.stdin.on("error", (err) => {
3405
+ clearTimeout(timer);
3406
+ reject(err);
3407
+ });
3408
+ process.stdin.resume();
3409
+ });
3410
+ }
3411
+
3412
+ // src/learner/storage.ts
3413
+ init_constants();
3414
+ import fs22 from "fs";
3415
+ import path19 from "path";
3416
+ var MAX_RESPONSE_LENGTH = 2e3;
3417
+ var DEFAULT_STATE = {
3418
+ sessionId: null,
3419
+ eventCount: 0,
3420
+ lastAnalysisTimestamp: null
3421
+ };
3422
+ function ensureLearningDir() {
3423
+ if (!fs22.existsSync(LEARNING_DIR)) {
3424
+ fs22.mkdirSync(LEARNING_DIR, { recursive: true });
3425
+ }
3426
+ }
3427
+ function sessionFilePath() {
3428
+ return path19.join(LEARNING_DIR, LEARNING_SESSION_FILE);
3429
+ }
3430
+ function stateFilePath() {
3431
+ return path19.join(LEARNING_DIR, LEARNING_STATE_FILE);
3432
+ }
3433
+ function truncateResponse(response) {
3434
+ const str = JSON.stringify(response);
3435
+ if (str.length <= MAX_RESPONSE_LENGTH) return response;
3436
+ return { _truncated: str.slice(0, MAX_RESPONSE_LENGTH) };
3437
+ }
3438
+ function appendEvent(event) {
3439
+ ensureLearningDir();
3440
+ const truncated = { ...event, tool_response: truncateResponse(event.tool_response) };
3441
+ const filePath = sessionFilePath();
3442
+ fs22.appendFileSync(filePath, JSON.stringify(truncated) + "\n");
3443
+ const count = getEventCount();
3444
+ if (count > LEARNING_MAX_EVENTS) {
3445
+ const lines = fs22.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
3446
+ const kept = lines.slice(lines.length - LEARNING_MAX_EVENTS);
3447
+ fs22.writeFileSync(filePath, kept.join("\n") + "\n");
3448
+ }
3449
+ }
3450
+ function readAllEvents() {
3451
+ const filePath = sessionFilePath();
3452
+ if (!fs22.existsSync(filePath)) return [];
3453
+ const lines = fs22.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
3454
+ return lines.map((line) => JSON.parse(line));
3455
+ }
3456
+ function getEventCount() {
3457
+ const filePath = sessionFilePath();
3458
+ if (!fs22.existsSync(filePath)) return 0;
3459
+ const content = fs22.readFileSync(filePath, "utf-8");
3460
+ return content.split("\n").filter(Boolean).length;
3461
+ }
3462
+ function clearSession() {
3463
+ const filePath = sessionFilePath();
3464
+ if (fs22.existsSync(filePath)) fs22.unlinkSync(filePath);
3465
+ }
3466
+ function readState2() {
3467
+ const filePath = stateFilePath();
3468
+ if (!fs22.existsSync(filePath)) return { ...DEFAULT_STATE };
3469
+ try {
3470
+ return JSON.parse(fs22.readFileSync(filePath, "utf-8"));
3471
+ } catch {
3472
+ return { ...DEFAULT_STATE };
3473
+ }
3474
+ }
3475
+ function writeState2(state) {
3476
+ ensureLearningDir();
3477
+ fs22.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
3478
+ }
3479
+ function resetState() {
3480
+ writeState2({ ...DEFAULT_STATE });
3481
+ }
3482
+
3483
+ // src/learner/writer.ts
3484
+ import fs23 from "fs";
3485
+ import path20 from "path";
3486
+ var LEARNED_START = "<!-- caliber:learned -->";
3487
+ var LEARNED_END = "<!-- /caliber:learned -->";
3488
+ function writeLearnedContent(update) {
3489
+ const written = [];
3490
+ if (update.claudeMdLearnedSection) {
3491
+ writeLearnedSection(update.claudeMdLearnedSection);
3492
+ written.push("CLAUDE.md");
3493
+ }
3494
+ if (update.skills?.length) {
3495
+ for (const skill of update.skills) {
3496
+ const skillPath = writeLearnedSkill(skill);
3497
+ written.push(skillPath);
3498
+ }
3499
+ }
3500
+ return written;
3501
+ }
3502
+ function writeLearnedSection(content) {
3503
+ const claudeMdPath = "CLAUDE.md";
3504
+ let existing = "";
3505
+ if (fs23.existsSync(claudeMdPath)) {
3506
+ existing = fs23.readFileSync(claudeMdPath, "utf-8");
3507
+ }
3508
+ const section = `${LEARNED_START}
3509
+ ${content}
3510
+ ${LEARNED_END}`;
3511
+ const startIdx = existing.indexOf(LEARNED_START);
3512
+ const endIdx = existing.indexOf(LEARNED_END);
3513
+ let updated;
3514
+ if (startIdx !== -1 && endIdx !== -1) {
3515
+ updated = existing.slice(0, startIdx) + section + existing.slice(endIdx + LEARNED_END.length);
3516
+ } else {
3517
+ const separator = existing.endsWith("\n") || existing === "" ? "" : "\n";
3518
+ updated = existing + separator + "\n" + section + "\n";
3519
+ }
3520
+ fs23.writeFileSync(claudeMdPath, updated);
3521
+ }
3522
+ function writeLearnedSkill(skill) {
3523
+ const skillDir = path20.join(".claude", "skills", skill.name);
3524
+ if (!fs23.existsSync(skillDir)) fs23.mkdirSync(skillDir, { recursive: true });
3525
+ const skillPath = path20.join(skillDir, "SKILL.md");
3526
+ if (!skill.isNew && fs23.existsSync(skillPath)) {
3527
+ const existing = fs23.readFileSync(skillPath, "utf-8");
3528
+ fs23.writeFileSync(skillPath, existing.trimEnd() + "\n\n" + skill.content);
3529
+ } else {
3530
+ const frontmatter = [
3531
+ "---",
3532
+ `name: ${skill.name}`,
3533
+ `description: ${skill.description}`,
3534
+ "---",
3535
+ ""
3536
+ ].join("\n");
3537
+ fs23.writeFileSync(skillPath, frontmatter + skill.content);
3538
+ }
3539
+ return skillPath;
3540
+ }
3541
+ function readLearnedSection() {
3542
+ const claudeMdPath = "CLAUDE.md";
3543
+ if (!fs23.existsSync(claudeMdPath)) return null;
3544
+ const content = fs23.readFileSync(claudeMdPath, "utf-8");
3545
+ const startIdx = content.indexOf(LEARNED_START);
3546
+ const endIdx = content.indexOf(LEARNED_END);
3547
+ if (startIdx === -1 || endIdx === -1) return null;
3548
+ return content.slice(startIdx + LEARNED_START.length, endIdx).trim();
3549
+ }
3550
+
3551
+ // src/lib/learning-hooks.ts
3552
+ import fs24 from "fs";
3553
+ import path21 from "path";
3554
+ var SETTINGS_PATH2 = path21.join(".claude", "settings.json");
3555
+ var HOOK_CONFIGS = [
3556
+ {
3557
+ event: "PostToolUse",
3558
+ command: "caliber learn observe",
3559
+ description: "Caliber: recording tool usage for session learning"
3560
+ },
3561
+ {
3562
+ event: "PostToolUseFailure",
3563
+ command: "caliber learn observe --failure",
3564
+ description: "Caliber: recording tool failure for session learning"
3565
+ },
3566
+ {
3567
+ event: "SessionEnd",
3568
+ command: "caliber learn finalize",
3569
+ description: "Caliber: finalizing session learnings"
3570
+ }
3571
+ ];
3572
+ function readSettings2() {
3573
+ if (!fs24.existsSync(SETTINGS_PATH2)) return {};
3574
+ try {
3575
+ return JSON.parse(fs24.readFileSync(SETTINGS_PATH2, "utf-8"));
3576
+ } catch {
3577
+ return {};
3578
+ }
3579
+ }
3580
+ function writeSettings2(settings) {
3581
+ const dir = path21.dirname(SETTINGS_PATH2);
3582
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
3583
+ fs24.writeFileSync(SETTINGS_PATH2, JSON.stringify(settings, null, 2));
3584
+ }
3585
+ function hasLearningHook(matchers, command) {
3586
+ return matchers.some((entry) => entry.hooks?.some((h) => h.command === command));
3587
+ }
3588
+ function areLearningHooksInstalled() {
3589
+ const settings = readSettings2();
3590
+ if (!settings.hooks) return false;
3591
+ return HOOK_CONFIGS.every((cfg) => {
3592
+ const matchers = settings.hooks[cfg.event];
3593
+ return Array.isArray(matchers) && hasLearningHook(matchers, cfg.command);
3594
+ });
3595
+ }
3596
+ function installLearningHooks() {
3597
+ if (areLearningHooksInstalled()) {
3598
+ return { installed: false, alreadyInstalled: true };
3599
+ }
3600
+ const settings = readSettings2();
3601
+ if (!settings.hooks) settings.hooks = {};
3602
+ for (const cfg of HOOK_CONFIGS) {
3603
+ if (!Array.isArray(settings.hooks[cfg.event])) {
3604
+ settings.hooks[cfg.event] = [];
3605
+ }
3606
+ if (!hasLearningHook(settings.hooks[cfg.event], cfg.command)) {
3607
+ settings.hooks[cfg.event].push({
3608
+ matcher: "",
3609
+ hooks: [{ type: "command", command: cfg.command, description: cfg.description }]
3610
+ });
3611
+ }
3612
+ }
3613
+ writeSettings2(settings);
3614
+ return { installed: true, alreadyInstalled: false };
3615
+ }
3616
+ function removeLearningHooks() {
3617
+ const settings = readSettings2();
3618
+ if (!settings.hooks) return { removed: false, notFound: true };
3619
+ let removedAny = false;
3620
+ for (const cfg of HOOK_CONFIGS) {
3621
+ const matchers = settings.hooks[cfg.event];
3622
+ if (!Array.isArray(matchers)) continue;
3623
+ const idx = matchers.findIndex((entry) => entry.hooks?.some((h) => h.command === cfg.command));
3624
+ if (idx !== -1) {
3625
+ matchers.splice(idx, 1);
3626
+ removedAny = true;
3627
+ if (matchers.length === 0) delete settings.hooks[cfg.event];
3628
+ }
3629
+ }
3630
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) {
3631
+ delete settings.hooks;
3632
+ }
3633
+ if (!removedAny) return { removed: false, notFound: true };
3634
+ writeSettings2(settings);
3635
+ return { removed: true, notFound: false };
3636
+ }
3637
+
3638
+ // src/commands/learn.ts
3639
+ async function learnObserveCommand(options) {
3640
+ try {
3641
+ const raw = await readStdin();
3642
+ if (!raw.trim()) return;
3643
+ const hookData = JSON.parse(raw);
3644
+ const event = {
3645
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3646
+ session_id: hookData.session_id || "unknown",
3647
+ hook_event_name: options.failure ? "PostToolUseFailure" : "PostToolUse",
3648
+ tool_name: hookData.tool_name || "unknown",
3649
+ tool_input: hookData.tool_input || {},
3650
+ tool_response: hookData.tool_response || {},
3651
+ tool_use_id: hookData.tool_use_id || "",
3652
+ cwd: hookData.cwd || process.cwd()
3653
+ };
3654
+ appendEvent(event);
3655
+ const state = readState2();
3656
+ state.eventCount++;
3657
+ if (!state.sessionId) state.sessionId = event.session_id;
3658
+ writeState2(state);
3659
+ } catch {
3660
+ }
3661
+ }
3662
+ async function learnFinalizeCommand() {
3663
+ try {
3664
+ const auth2 = getStoredAuth();
3665
+ if (!auth2) {
3666
+ clearSession();
3667
+ resetState();
3668
+ return;
3669
+ }
3670
+ const events = readAllEvents();
3671
+ if (!events.length) {
3672
+ clearSession();
3673
+ resetState();
3674
+ return;
3675
+ }
3676
+ const existingConfigs = readExistingConfigs(process.cwd());
3677
+ const existingLearnedSection = readLearnedSection();
3678
+ const existingSkills = existingConfigs.claudeSkills || [];
3679
+ const response = await apiRequest("/api/learn/analyze", {
3680
+ method: "POST",
3681
+ body: {
3682
+ events,
3683
+ existingClaudeMd: existingConfigs.claudeMd || "",
3684
+ existingLearnedSection,
3685
+ existingSkills
3686
+ }
3687
+ });
3688
+ if (response.claudeMdLearnedSection || response.skills?.length) {
3689
+ writeLearnedContent({
3690
+ claudeMdLearnedSection: response.claudeMdLearnedSection,
3691
+ skills: response.skills
3692
+ });
3693
+ }
3694
+ } catch {
3695
+ } finally {
3696
+ clearSession();
3697
+ resetState();
3698
+ }
3699
+ }
3700
+ async function learnInstallCommand() {
3701
+ const result = installLearningHooks();
3702
+ if (result.alreadyInstalled) {
3703
+ console.log(chalk14.dim("Learning hooks already installed."));
3704
+ return;
3705
+ }
3706
+ console.log(chalk14.green("\u2713") + " Learning hooks installed in .claude/settings.json");
3707
+ console.log(chalk14.dim(" PostToolUse, PostToolUseFailure, and SessionEnd hooks active."));
3708
+ console.log(chalk14.dim(" Session learnings will be written to CLAUDE.md and skills."));
3709
+ }
3710
+ async function learnRemoveCommand() {
3711
+ const result = removeLearningHooks();
3712
+ if (result.notFound) {
3713
+ console.log(chalk14.dim("Learning hooks not found."));
3714
+ return;
3715
+ }
3716
+ console.log(chalk14.green("\u2713") + " Learning hooks removed from .claude/settings.json");
3717
+ }
3718
+ async function learnStatusCommand() {
3719
+ const installed = areLearningHooksInstalled();
3720
+ const state = readState2();
3721
+ const eventCount = getEventCount();
3722
+ console.log(chalk14.bold("Session Learning Status"));
3723
+ console.log();
3724
+ if (installed) {
3725
+ console.log(chalk14.green("\u2713") + " Learning hooks are " + chalk14.green("installed"));
3726
+ } else {
3727
+ console.log(chalk14.dim("\u2717") + " Learning hooks are " + chalk14.yellow("not installed"));
3728
+ console.log(chalk14.dim(" Run `caliber learn install` to enable session learning."));
3729
+ }
3730
+ console.log();
3731
+ console.log(`Events recorded: ${chalk14.cyan(String(eventCount))}`);
3732
+ console.log(`Total this session: ${chalk14.cyan(String(state.eventCount))}`);
3733
+ if (state.lastAnalysisTimestamp) {
3734
+ console.log(`Last analysis: ${chalk14.cyan(state.lastAnalysisTimestamp)}`);
3735
+ } else {
3736
+ console.log(`Last analysis: ${chalk14.dim("none")}`);
3737
+ }
3738
+ const learnedSection = readLearnedSection();
3739
+ if (learnedSection) {
3740
+ const lineCount = learnedSection.split("\n").filter(Boolean).length;
3741
+ console.log(`
3742
+ Learned items in CLAUDE.md: ${chalk14.cyan(String(lineCount))}`);
3743
+ }
3744
+ }
3745
+
3357
3746
  // src/cli.ts
3358
- var __dirname2 = path19.dirname(fileURLToPath3(import.meta.url));
3747
+ var __dirname2 = path22.dirname(fileURLToPath3(import.meta.url));
3359
3748
  var pkg3 = JSON.parse(
3360
- fs22.readFileSync(path19.resolve(__dirname2, "..", "package.json"), "utf-8")
3749
+ fs25.readFileSync(path22.resolve(__dirname2, "..", "package.json"), "utf-8")
3361
3750
  );
3362
3751
  var program = new Command();
3363
3752
  var displayVersion = process.env.CALIBER_LOCAL ? `${pkg3.version}-local` : pkg3.version;
@@ -3377,24 +3766,30 @@ var hooks = program.command("hooks").description("Manage Claude Code session hoo
3377
3766
  hooks.command("install").description("Install auto-refresh SessionEnd hook").action(hooksInstallCommand);
3378
3767
  hooks.command("remove").description("Remove auto-refresh SessionEnd hook").action(hooksRemoveCommand);
3379
3768
  hooks.command("status").description("Check if auto-refresh hook is installed").action(hooksStatusCommand);
3769
+ var learn = program.command("learn").description("Session learning \u2014 observe tool usage and extract reusable instructions");
3770
+ learn.command("observe").description("Record a tool event from stdin (called by hooks)").option("--failure", "Mark event as a tool failure").action(learnObserveCommand);
3771
+ learn.command("finalize").description("Analyze session events and update CLAUDE.md (called on SessionEnd)").action(learnFinalizeCommand);
3772
+ learn.command("install").description("Install learning hooks into .claude/settings.json").action(learnInstallCommand);
3773
+ learn.command("remove").description("Remove learning hooks from .claude/settings.json").action(learnRemoveCommand);
3774
+ learn.command("status").description("Show learning system status").action(learnStatusCommand);
3380
3775
 
3381
3776
  // src/utils/version-check.ts
3382
- import fs23 from "fs";
3383
- import path20 from "path";
3777
+ import fs26 from "fs";
3778
+ import path23 from "path";
3384
3779
  import { fileURLToPath as fileURLToPath4 } from "url";
3385
3780
  import { execSync as execSync5 } from "child_process";
3386
- import chalk14 from "chalk";
3781
+ import chalk15 from "chalk";
3387
3782
  import ora10 from "ora";
3388
3783
  import confirm3 from "@inquirer/confirm";
3389
- var __dirname_vc = path20.dirname(fileURLToPath4(import.meta.url));
3784
+ var __dirname_vc = path23.dirname(fileURLToPath4(import.meta.url));
3390
3785
  var pkg4 = JSON.parse(
3391
- fs23.readFileSync(path20.resolve(__dirname_vc, "..", "package.json"), "utf-8")
3786
+ fs26.readFileSync(path23.resolve(__dirname_vc, "..", "package.json"), "utf-8")
3392
3787
  );
3393
3788
  function getInstalledVersion() {
3394
3789
  try {
3395
3790
  const globalRoot = execSync5("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3396
- const pkgPath = path20.join(globalRoot, "@caliber-ai", "cli", "package.json");
3397
- return JSON.parse(fs23.readFileSync(pkgPath, "utf-8")).version;
3791
+ const pkgPath = path23.join(globalRoot, "@caliber-ai", "cli", "package.json");
3792
+ return JSON.parse(fs26.readFileSync(pkgPath, "utf-8")).version;
3398
3793
  } catch {
3399
3794
  return null;
3400
3795
  }
@@ -3417,17 +3812,17 @@ async function checkForUpdates() {
3417
3812
  const isInteractive = process.stdin.isTTY === true;
3418
3813
  if (!isInteractive) {
3419
3814
  console.log(
3420
- chalk14.yellow(
3815
+ chalk15.yellow(
3421
3816
  `
3422
3817
  Update available: ${current} -> ${latest}
3423
- Run ${chalk14.bold("npm install -g @caliber-ai/cli")} to upgrade.
3818
+ Run ${chalk15.bold("npm install -g @caliber-ai/cli")} to upgrade.
3424
3819
  `
3425
3820
  )
3426
3821
  );
3427
3822
  return;
3428
3823
  }
3429
3824
  console.log(
3430
- chalk14.yellow(`
3825
+ chalk15.yellow(`
3431
3826
  Update available: ${current} -> ${latest}`)
3432
3827
  );
3433
3828
  const shouldUpdate = await confirm3({ message: "Would you like to update now? (Y/n)", default: true });
@@ -3441,13 +3836,13 @@ Update available: ${current} -> ${latest}`)
3441
3836
  const installed = getInstalledVersion();
3442
3837
  if (installed !== latest) {
3443
3838
  spinner.fail(`Update incomplete \u2014 got ${installed ?? "unknown"}, expected ${latest}`);
3444
- console.log(chalk14.yellow(`Run ${chalk14.bold(`npm install -g @caliber-ai/cli@${latest}`)} manually.
3839
+ console.log(chalk15.yellow(`Run ${chalk15.bold(`npm install -g @caliber-ai/cli@${latest}`)} manually.
3445
3840
  `));
3446
3841
  return;
3447
3842
  }
3448
- spinner.succeed(chalk14.green(`Updated to ${latest}`));
3843
+ spinner.succeed(chalk15.green(`Updated to ${latest}`));
3449
3844
  const args = process.argv.slice(2);
3450
- console.log(chalk14.dim(`
3845
+ console.log(chalk15.dim(`
3451
3846
  Restarting: caliber ${args.join(" ")}
3452
3847
  `));
3453
3848
  execSync5(`caliber ${args.map((a) => JSON.stringify(a)).join(" ")}`, {
@@ -3458,10 +3853,10 @@ Restarting: caliber ${args.join(" ")}
3458
3853
  } catch (err) {
3459
3854
  spinner.fail("Update failed");
3460
3855
  const msg = err instanceof Error ? err.message : "";
3461
- if (msg && !msg.includes("SIGTERM")) console.log(chalk14.dim(` ${msg.split("\n")[0]}`));
3856
+ if (msg && !msg.includes("SIGTERM")) console.log(chalk15.dim(` ${msg.split("\n")[0]}`));
3462
3857
  console.log(
3463
- chalk14.yellow(
3464
- `Run ${chalk14.bold(`npm install -g @caliber-ai/cli@${latest}`)} manually to upgrade.
3858
+ chalk15.yellow(
3859
+ `Run ${chalk15.bold(`npm install -g @caliber-ai/cli@${latest}`)} manually to upgrade.
3465
3860
  `
3466
3861
  )
3467
3862
  );