learning-agent 0.1.0 → 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.
package/dist/cli.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import chalk from 'chalk';
3
3
  import { Command } from 'commander';
4
- import { statSync, mkdirSync } from 'fs';
5
- import { join, dirname } from 'path';
4
+ import { existsSync, statSync, chmodSync, mkdirSync } from 'fs';
6
5
  import * as fs from 'fs/promises';
7
- import { mkdir, appendFile, readFile, writeFile, rename } from 'fs/promises';
6
+ import { mkdir, writeFile, readFile, rename, appendFile } from 'fs/promises';
7
+ import { homedir } from 'os';
8
+ import { join, dirname } from 'path';
8
9
  import { createHash } from 'crypto';
9
10
  import Database from 'better-sqlite3';
10
11
  import { z } from 'zod';
11
- import 'node-llama-cpp';
12
- import { homedir } from 'os';
12
+ import { getLlama, resolveModelFile } from 'node-llama-cpp';
13
13
 
14
14
  var SourceSchema = z.enum([
15
15
  "user_correction",
@@ -216,6 +216,28 @@ function openDb(repoRoot) {
216
216
  createSchema(db);
217
217
  return db;
218
218
  }
219
+ function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
220
+ const database = openDb(repoRoot);
221
+ const row = database.prepare("SELECT embedding, content_hash FROM lessons WHERE id = ?").get(lessonId);
222
+ if (!row || !row.embedding || !row.content_hash) {
223
+ return null;
224
+ }
225
+ if (expectedHash && row.content_hash !== expectedHash) {
226
+ return null;
227
+ }
228
+ const float32 = new Float32Array(
229
+ row.embedding.buffer,
230
+ row.embedding.byteOffset,
231
+ row.embedding.byteLength / 4
232
+ );
233
+ return Array.from(float32);
234
+ }
235
+ function setCachedEmbedding(repoRoot, lessonId, embedding, hash) {
236
+ const database = openDb(repoRoot);
237
+ const float32 = embedding instanceof Float32Array ? embedding : new Float32Array(embedding);
238
+ const buffer = Buffer.from(float32.buffer, float32.byteOffset, float32.byteLength);
239
+ database.prepare("UPDATE lessons SET embedding = ?, content_hash = ? WHERE id = ?").run(buffer, hash, lessonId);
240
+ }
219
241
  function rowToLesson(row) {
220
242
  const lesson = {
221
243
  id: row.id,
@@ -547,8 +569,6 @@ function detectTestFailure(testResult) {
547
569
  trigger: `Test failure in ${testResult.testFile}: ${errorLine.slice(0, 100)}`
548
570
  };
549
571
  }
550
-
551
- // src/capture/integration.ts
552
572
  async function detectAndPropose(repoRoot, input) {
553
573
  const detected = runDetector(input);
554
574
  if (!detected) {
@@ -634,7 +654,152 @@ function parseLimit(value, name) {
634
654
  function getRepoRoot() {
635
655
  return process.env["LEARNING_AGENT_ROOT"] ?? process.cwd();
636
656
  }
637
- join(homedir(), ".node-llama-cpp", "models");
657
+ var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
658
+ var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
659
+ var DEFAULT_MODEL_DIR = join(homedir(), ".node-llama-cpp", "models");
660
+ function isModelAvailable() {
661
+ return existsSync(join(DEFAULT_MODEL_DIR, MODEL_FILENAME));
662
+ }
663
+ async function resolveModel(options = {}) {
664
+ const { cli = true } = options;
665
+ return resolveModelFile(MODEL_URI, { cli });
666
+ }
667
+
668
+ // src/embeddings/nomic.ts
669
+ var embeddingContext = null;
670
+ async function getEmbedding() {
671
+ if (embeddingContext) return embeddingContext;
672
+ const modelPath = await resolveModel({ cli: true });
673
+ const llama = await getLlama();
674
+ const model = await llama.loadModel({ modelPath });
675
+ embeddingContext = await model.createEmbeddingContext();
676
+ return embeddingContext;
677
+ }
678
+ async function embedText(text) {
679
+ const ctx = await getEmbedding();
680
+ const result = await ctx.getEmbeddingFor(text);
681
+ return Array.from(result.vector);
682
+ }
683
+
684
+ // src/search/vector.ts
685
+ function cosineSimilarity(a, b) {
686
+ if (a.length !== b.length) {
687
+ throw new Error("Vectors must have same length");
688
+ }
689
+ let dotProduct = 0;
690
+ let normA = 0;
691
+ let normB = 0;
692
+ for (let i = 0; i < a.length; i++) {
693
+ dotProduct += a[i] * b[i];
694
+ normA += a[i] * a[i];
695
+ normB += b[i] * b[i];
696
+ }
697
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
698
+ if (magnitude === 0) return 0;
699
+ return dotProduct / magnitude;
700
+ }
701
+ var DEFAULT_LIMIT = 10;
702
+ async function searchVector(repoRoot, query, options) {
703
+ const limit = options?.limit ?? DEFAULT_LIMIT;
704
+ const { lessons } = await readLessons(repoRoot);
705
+ if (lessons.length === 0) return [];
706
+ const queryVector = await embedText(query);
707
+ const scored = [];
708
+ for (const lesson of lessons) {
709
+ const lessonText = `${lesson.trigger} ${lesson.insight}`;
710
+ const hash = contentHash(lesson.trigger, lesson.insight);
711
+ let lessonVector = getCachedEmbedding(repoRoot, lesson.id, hash);
712
+ if (!lessonVector) {
713
+ lessonVector = await embedText(lessonText);
714
+ setCachedEmbedding(repoRoot, lesson.id, lessonVector, hash);
715
+ }
716
+ const score = cosineSimilarity(queryVector, lessonVector);
717
+ scored.push({ lesson, score });
718
+ }
719
+ scored.sort((a, b) => b.score - a.score);
720
+ return scored.slice(0, limit);
721
+ }
722
+
723
+ // src/search/ranking.ts
724
+ var RECENCY_THRESHOLD_DAYS = 30;
725
+ var HIGH_SEVERITY_BOOST = 1.5;
726
+ var MEDIUM_SEVERITY_BOOST = 1;
727
+ var LOW_SEVERITY_BOOST = 0.8;
728
+ var RECENCY_BOOST = 1.2;
729
+ var CONFIRMATION_BOOST = 1.3;
730
+ function severityBoost(lesson) {
731
+ switch (lesson.severity) {
732
+ case "high":
733
+ return HIGH_SEVERITY_BOOST;
734
+ case "medium":
735
+ return MEDIUM_SEVERITY_BOOST;
736
+ case "low":
737
+ return LOW_SEVERITY_BOOST;
738
+ default:
739
+ return MEDIUM_SEVERITY_BOOST;
740
+ }
741
+ }
742
+ function recencyBoost(lesson) {
743
+ const created = new Date(lesson.created);
744
+ const now = /* @__PURE__ */ new Date();
745
+ const ageMs = now.getTime() - created.getTime();
746
+ const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
747
+ return ageDays <= RECENCY_THRESHOLD_DAYS ? RECENCY_BOOST : 1;
748
+ }
749
+ function confirmationBoost(lesson) {
750
+ return lesson.confirmed ? CONFIRMATION_BOOST : 1;
751
+ }
752
+ function calculateScore(lesson, vectorSimilarity) {
753
+ return vectorSimilarity * severityBoost(lesson) * recencyBoost(lesson) * confirmationBoost(lesson);
754
+ }
755
+ function rankLessons(lessons) {
756
+ return lessons.map((scored) => ({
757
+ ...scored,
758
+ finalScore: calculateScore(scored.lesson, scored.score)
759
+ })).sort((a, b) => (b.finalScore ?? 0) - (a.finalScore ?? 0));
760
+ }
761
+
762
+ // src/retrieval/session.ts
763
+ var DEFAULT_LIMIT2 = 5;
764
+ function isFullLesson(lesson) {
765
+ return lesson.type === "full" && lesson.severity !== void 0;
766
+ }
767
+ async function loadSessionLessons(repoRoot, limit = DEFAULT_LIMIT2) {
768
+ const { lessons: allLessons } = await readLessons(repoRoot);
769
+ const highSeverityLessons = allLessons.filter(
770
+ (lesson) => isFullLesson(lesson) && lesson.severity === "high" && lesson.confirmed
771
+ );
772
+ highSeverityLessons.sort((a, b) => {
773
+ const dateA = new Date(a.created).getTime();
774
+ const dateB = new Date(b.created).getTime();
775
+ return dateB - dateA;
776
+ });
777
+ return highSeverityLessons.slice(0, limit);
778
+ }
779
+
780
+ // src/retrieval/plan.ts
781
+ var DEFAULT_LIMIT3 = 5;
782
+ async function retrieveForPlan(repoRoot, planText, limit = DEFAULT_LIMIT3) {
783
+ const scored = await searchVector(repoRoot, planText, { limit: limit * 2 });
784
+ const ranked = rankLessons(scored);
785
+ const topLessons = ranked.slice(0, limit);
786
+ const message = formatLessonsCheck(topLessons);
787
+ return { lessons: topLessons, message };
788
+ }
789
+ function formatLessonsCheck(lessons) {
790
+ const header = "\u{1F4DA} Lessons Check\n" + "\u2500".repeat(40);
791
+ if (lessons.length === 0) {
792
+ return `${header}
793
+ No relevant lessons found for this plan.`;
794
+ }
795
+ const lessonLines = lessons.map((l, i) => {
796
+ const bullet = `${i + 1}.`;
797
+ const insight = l.lesson.insight;
798
+ return `${bullet} ${insight}`;
799
+ });
800
+ return `${header}
801
+ ${lessonLines.join("\n")}`;
802
+ }
638
803
 
639
804
  // src/index.ts
640
805
  var VERSION = "0.1.0";
@@ -753,6 +918,28 @@ async function compact(repoRoot) {
753
918
  }
754
919
 
755
920
  // src/cli.ts
921
+ var PRE_COMMIT_MESSAGE = `Before committing, have you captured any valuable lessons from this session?
922
+ Consider: corrections, mistakes, or insights worth remembering.
923
+
924
+ To capture a lesson:
925
+ npx learning-agent capture --trigger "what happened" --insight "what to do" --yes`;
926
+ var PRE_COMMIT_HOOK_TEMPLATE = `#!/bin/sh
927
+ # Learning Agent pre-commit hook
928
+ # Reminds Claude to consider capturing lessons before commits
929
+
930
+ npx learning-agent hooks run pre-commit
931
+ `;
932
+ var CLAUDE_HOOK_MARKER = "learning-agent load-session";
933
+ var CLAUDE_HOOK_CONFIG = {
934
+ matcher: "startup|resume|compact",
935
+ hooks: [
936
+ {
937
+ type: "command",
938
+ command: "npx learning-agent load-session 2>/dev/null || true"
939
+ }
940
+ ]
941
+ };
942
+ var HOOK_MARKER = "# Learning Agent pre-commit hook";
756
943
  var out = {
757
944
  success: (msg) => console.log(chalk.green("[ok]"), msg),
758
945
  error: (msg) => console.error(chalk.red("[error]"), msg),
@@ -768,9 +955,457 @@ function getGlobalOpts(cmd) {
768
955
  }
769
956
  var DEFAULT_SEARCH_LIMIT = "10";
770
957
  var DEFAULT_LIST_LIMIT = "20";
958
+ var DEFAULT_CHECK_PLAN_LIMIT = "5";
959
+ var ISO_DATE_PREFIX_LENGTH = 10;
960
+ var AVG_DECIMAL_PLACES = 1;
961
+ var RELEVANCE_DECIMAL_PLACES = 2;
962
+ var JSON_INDENT_SPACES = 2;
963
+ function createLessonFromFlags(trigger, insight, confirmed) {
964
+ return {
965
+ id: generateId(insight),
966
+ type: "quick",
967
+ trigger,
968
+ insight,
969
+ tags: [],
970
+ source: "manual",
971
+ context: { tool: "capture", intent: "manual capture" },
972
+ created: (/* @__PURE__ */ new Date()).toISOString(),
973
+ confirmed,
974
+ supersedes: [],
975
+ related: []
976
+ };
977
+ }
978
+ function outputCaptureJson(lesson, saved) {
979
+ console.log(JSON.stringify({
980
+ id: lesson.id,
981
+ trigger: lesson.trigger,
982
+ insight: lesson.insight,
983
+ type: lesson.type,
984
+ saved
985
+ }));
986
+ }
987
+ function outputCapturePreview(lesson) {
988
+ console.log("Lesson captured:");
989
+ console.log(` ID: ${lesson.id}`);
990
+ console.log(` Trigger: ${lesson.trigger}`);
991
+ console.log(` Insight: ${lesson.insight}`);
992
+ console.log(` Type: ${lesson.type}`);
993
+ console.log(` Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
994
+ console.log("\nSave this lesson? [y/n]");
995
+ }
996
+ function createLessonFromInputFile(result, confirmed) {
997
+ return {
998
+ id: generateId(result.proposedInsight),
999
+ type: "quick",
1000
+ trigger: result.trigger,
1001
+ insight: result.proposedInsight,
1002
+ tags: [],
1003
+ source: result.source,
1004
+ context: { tool: "capture", intent: "auto-capture" },
1005
+ created: (/* @__PURE__ */ new Date()).toISOString(),
1006
+ confirmed,
1007
+ supersedes: [],
1008
+ related: []
1009
+ };
1010
+ }
1011
+ async function readPlanFromStdin() {
1012
+ const { stdin } = await import('process');
1013
+ if (!stdin.isTTY) {
1014
+ const chunks = [];
1015
+ for await (const chunk of stdin) {
1016
+ chunks.push(chunk);
1017
+ }
1018
+ return Buffer.concat(chunks).toString("utf-8").trim();
1019
+ }
1020
+ return void 0;
1021
+ }
1022
+ function outputCheckPlanJson(lessons) {
1023
+ const jsonOutput = {
1024
+ lessons: lessons.map((l) => ({
1025
+ id: l.lesson.id,
1026
+ insight: l.lesson.insight,
1027
+ relevance: l.score,
1028
+ source: l.lesson.source
1029
+ })),
1030
+ count: lessons.length
1031
+ };
1032
+ console.log(JSON.stringify(jsonOutput));
1033
+ }
1034
+ function outputCheckPlanHuman(lessons, quiet) {
1035
+ console.log("## Lessons Check\n");
1036
+ console.log("Relevant to your plan:\n");
1037
+ lessons.forEach((item, i) => {
1038
+ const num = i + 1;
1039
+ console.log(`${num}. ${chalk.bold(`[${item.lesson.id}]`)} ${item.lesson.insight}`);
1040
+ console.log(` - Relevance: ${item.score.toFixed(RELEVANCE_DECIMAL_PLACES)}`);
1041
+ console.log(` - Source: ${item.lesson.source}`);
1042
+ console.log();
1043
+ });
1044
+ if (!quiet) {
1045
+ console.log("---");
1046
+ console.log("Consider these lessons while implementing.");
1047
+ }
1048
+ }
1049
+ function outputSessionLessonsHuman(lessons, quiet) {
1050
+ console.log("## Session Lessons (High Severity)\n");
1051
+ lessons.forEach((lesson, i) => {
1052
+ const num = i + 1;
1053
+ const date = lesson.created.slice(0, ISO_DATE_PREFIX_LENGTH);
1054
+ console.log(`${num}. ${chalk.bold(`[${lesson.id}]`)} ${lesson.insight}`);
1055
+ console.log(` - Source: ${lesson.source} (${date})`);
1056
+ if (lesson.tags.length > 0) {
1057
+ console.log(` - Tags: ${lesson.tags.join(", ")}`);
1058
+ }
1059
+ console.log();
1060
+ });
1061
+ const lessonWord = lessons.length === 1 ? "lesson" : "lessons";
1062
+ if (!quiet) {
1063
+ console.log("---");
1064
+ console.log(`${lessons.length} high-severity ${lessonWord} loaded.`);
1065
+ }
1066
+ }
1067
+ var LEARNING_AGENT_SECTION_HEADER = "## Learning Agent Integration";
1068
+ var AGENTS_MD_TEMPLATE = `
1069
+ ## Learning Agent Integration
1070
+
1071
+ This project uses learning-agent for session memory.
1072
+
1073
+ ### Retrieval Points
1074
+
1075
+ - **Session start**: High-severity lessons loaded automatically
1076
+ - **Plan-time**: BEFORE implementing a plan, run check-plan to retrieve relevant lessons
1077
+
1078
+ ### Plan-Time Retrieval (Explicit Step)
1079
+
1080
+ **BEFORE implementing any plan**, run:
1081
+
1082
+ \`\`\`bash
1083
+ npx learning-agent check-plan --plan "your plan description" --json
1084
+ \`\`\`
1085
+
1086
+ Display results as a **Lessons Check** section after your plan:
1087
+
1088
+ \`\`\`
1089
+ ## Lessons Check
1090
+ 1. [insight from lesson 1] (relevance: 0.85)
1091
+ 2. [insight from lesson 2] (relevance: 0.72)
1092
+ \`\`\`
1093
+
1094
+ Consider each lesson while implementing.
1095
+
1096
+ ### Proposing Lessons
1097
+
1098
+ Propose when: user correction, self-correction, test failure fix, or manual request.
1099
+
1100
+ **Quality gate (ALL must pass):**
1101
+
1102
+ - Novel (not already stored)
1103
+ - Specific (clear guidance)
1104
+ - Actionable (obvious what to do)
1105
+
1106
+ **Confirmation format:**
1107
+
1108
+ \`\`\`
1109
+ Learned: [insight]. Save? [y/n]
1110
+ \`\`\`
1111
+
1112
+ ### Session-End Protocol
1113
+
1114
+ Before closing a session, reflect on lessons learned:
1115
+
1116
+ 1. **Review**: What mistakes or corrections happened?
1117
+ 2. **Quality gate**: Is it novel, specific, actionable?
1118
+ 3. **Propose**: "Learned: [insight]. Save? [y/n]"
1119
+ 4. **Capture**: \`npx learning-agent capture --trigger "..." --insight "..." --yes\`
1120
+
1121
+ ### CLI Commands
1122
+
1123
+ \`\`\`bash
1124
+ npx learning-agent load-session --json # Session start
1125
+ npx learning-agent check-plan --plan "..." --json # Before implementing
1126
+ npx learning-agent capture --trigger "..." --insight "..." --yes
1127
+ \`\`\`
1128
+
1129
+ See [AGENTS.md](https://github.com/Nathandela/learning_agent/blob/main/AGENTS.md) for full documentation.
1130
+ `;
1131
+ function hasLearningAgentSection(content) {
1132
+ return content.includes(LEARNING_AGENT_SECTION_HEADER);
1133
+ }
1134
+ async function createLessonsDirectory(repoRoot) {
1135
+ const lessonsDir = dirname(join(repoRoot, LESSONS_PATH));
1136
+ await mkdir(lessonsDir, { recursive: true });
1137
+ }
1138
+ async function createIndexFile(repoRoot) {
1139
+ const indexPath = join(repoRoot, LESSONS_PATH);
1140
+ if (!existsSync(indexPath)) {
1141
+ await writeFile(indexPath, "", "utf-8");
1142
+ }
1143
+ }
1144
+ async function updateAgentsMd(repoRoot) {
1145
+ const agentsPath = join(repoRoot, "AGENTS.md");
1146
+ let content = "";
1147
+ let existed = false;
1148
+ if (existsSync(agentsPath)) {
1149
+ content = await readFile(agentsPath, "utf-8");
1150
+ existed = true;
1151
+ if (hasLearningAgentSection(content)) {
1152
+ return false;
1153
+ }
1154
+ }
1155
+ const newContent = existed ? content.trimEnd() + "\n" + AGENTS_MD_TEMPLATE : AGENTS_MD_TEMPLATE.trim() + "\n";
1156
+ await writeFile(agentsPath, newContent, "utf-8");
1157
+ return true;
1158
+ }
1159
+ var HOOK_FILE_MODE = 493;
1160
+ function hasLearningAgentHook(content) {
1161
+ return content.includes(HOOK_MARKER);
1162
+ }
1163
+ async function getGitHooksDir(repoRoot) {
1164
+ const gitDir = join(repoRoot, ".git");
1165
+ if (!existsSync(gitDir)) {
1166
+ return null;
1167
+ }
1168
+ const configPath = join(gitDir, "config");
1169
+ if (existsSync(configPath)) {
1170
+ const config = await readFile(configPath, "utf-8");
1171
+ const match = /hooksPath\s*=\s*(.+)$/m.exec(config);
1172
+ if (match?.[1]) {
1173
+ const hooksPath = match[1].trim();
1174
+ return hooksPath.startsWith("/") ? hooksPath : join(repoRoot, hooksPath);
1175
+ }
1176
+ }
1177
+ const defaultHooksDir = join(gitDir, "hooks");
1178
+ return existsSync(defaultHooksDir) ? defaultHooksDir : null;
1179
+ }
1180
+ var LEARNING_AGENT_HOOK_BLOCK = `
1181
+ # Learning Agent pre-commit hook (appended)
1182
+ npx learning-agent hooks run pre-commit
1183
+ `;
1184
+ async function installPreCommitHook(repoRoot) {
1185
+ const gitHooksDir = await getGitHooksDir(repoRoot);
1186
+ if (!gitHooksDir) {
1187
+ return false;
1188
+ }
1189
+ await mkdir(gitHooksDir, { recursive: true });
1190
+ const hookPath = join(gitHooksDir, "pre-commit");
1191
+ if (existsSync(hookPath)) {
1192
+ const content = await readFile(hookPath, "utf-8");
1193
+ if (hasLearningAgentHook(content)) {
1194
+ return false;
1195
+ }
1196
+ const newContent = content.trimEnd() + "\n" + LEARNING_AGENT_HOOK_BLOCK;
1197
+ await writeFile(hookPath, newContent, "utf-8");
1198
+ chmodSync(hookPath, HOOK_FILE_MODE);
1199
+ return true;
1200
+ }
1201
+ await writeFile(hookPath, PRE_COMMIT_HOOK_TEMPLATE, "utf-8");
1202
+ chmodSync(hookPath, HOOK_FILE_MODE);
1203
+ return true;
1204
+ }
771
1205
  var program = new Command();
772
1206
  program.option("-v, --verbose", "Show detailed output").option("-q, --quiet", "Suppress non-essential output");
773
1207
  program.name("learning-agent").description("Repository-scoped learning system for Claude Code").version(VERSION);
1208
+ program.command("init").description("Initialize learning-agent in this repository").option("--skip-agents", "Skip AGENTS.md modification").option("--skip-hooks", "Skip git hooks installation").option("--json", "Output result as JSON").action(async function(options) {
1209
+ const repoRoot = getRepoRoot();
1210
+ const { quiet } = getGlobalOpts(this);
1211
+ await createLessonsDirectory(repoRoot);
1212
+ await createIndexFile(repoRoot);
1213
+ const lessonsDir = dirname(join(repoRoot, LESSONS_PATH));
1214
+ let agentsMdUpdated = false;
1215
+ if (!options.skipAgents) {
1216
+ agentsMdUpdated = await updateAgentsMd(repoRoot);
1217
+ }
1218
+ let hooksInstalled = false;
1219
+ if (!options.skipHooks) {
1220
+ hooksInstalled = await installPreCommitHook(repoRoot);
1221
+ }
1222
+ if (options.json) {
1223
+ console.log(JSON.stringify({
1224
+ initialized: true,
1225
+ lessonsDir,
1226
+ agentsMd: agentsMdUpdated,
1227
+ hooks: hooksInstalled
1228
+ }));
1229
+ } else if (!quiet) {
1230
+ out.success("Learning agent initialized");
1231
+ console.log(` Lessons directory: ${lessonsDir}`);
1232
+ if (agentsMdUpdated) {
1233
+ console.log(" AGENTS.md: Updated with Learning Agent section");
1234
+ } else if (options.skipAgents) {
1235
+ console.log(" AGENTS.md: Skipped (--skip-agents)");
1236
+ } else {
1237
+ console.log(" AGENTS.md: Already has Learning Agent section");
1238
+ }
1239
+ if (hooksInstalled) {
1240
+ console.log(" Git hooks: pre-commit hook installed");
1241
+ } else if (options.skipHooks) {
1242
+ console.log(" Git hooks: Skipped (--skip-hooks)");
1243
+ } else {
1244
+ console.log(" Git hooks: Already installed or not a git repo");
1245
+ }
1246
+ }
1247
+ });
1248
+ var hooksCommand = program.command("hooks").description("Git hooks management");
1249
+ hooksCommand.command("run <hook>").description("Run a hook script (called by git hooks)").option("--json", "Output as JSON").action((hook, options) => {
1250
+ if (hook === "pre-commit") {
1251
+ if (options.json) {
1252
+ console.log(JSON.stringify({ hook: "pre-commit", message: PRE_COMMIT_MESSAGE }));
1253
+ } else {
1254
+ console.log(PRE_COMMIT_MESSAGE);
1255
+ }
1256
+ } else {
1257
+ if (options.json) {
1258
+ console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
1259
+ } else {
1260
+ out.error(`Unknown hook: ${hook}`);
1261
+ }
1262
+ process.exit(1);
1263
+ }
1264
+ });
1265
+ function getClaudeSettingsPath(project) {
1266
+ if (project) {
1267
+ const repoRoot = getRepoRoot();
1268
+ return join(repoRoot, ".claude", "settings.json");
1269
+ }
1270
+ return join(homedir(), ".claude", "settings.json");
1271
+ }
1272
+ async function readClaudeSettings(settingsPath) {
1273
+ if (!existsSync(settingsPath)) {
1274
+ return {};
1275
+ }
1276
+ const content = await readFile(settingsPath, "utf-8");
1277
+ return JSON.parse(content);
1278
+ }
1279
+ function hasClaudeHook(settings) {
1280
+ const hooks = settings.hooks;
1281
+ if (!hooks?.SessionStart) return false;
1282
+ return hooks.SessionStart.some((entry) => {
1283
+ const hookEntry = entry;
1284
+ return hookEntry.hooks?.some((h) => h.command?.includes(CLAUDE_HOOK_MARKER));
1285
+ });
1286
+ }
1287
+ function addLearningAgentHook(settings) {
1288
+ if (!settings.hooks) {
1289
+ settings.hooks = {};
1290
+ }
1291
+ const hooks = settings.hooks;
1292
+ if (!hooks.SessionStart) {
1293
+ hooks.SessionStart = [];
1294
+ }
1295
+ hooks.SessionStart.push(CLAUDE_HOOK_CONFIG);
1296
+ }
1297
+ function removeLearningAgentHook(settings) {
1298
+ const hooks = settings.hooks;
1299
+ if (!hooks?.SessionStart) return false;
1300
+ const originalLength = hooks.SessionStart.length;
1301
+ hooks.SessionStart = hooks.SessionStart.filter((entry) => {
1302
+ const hookEntry = entry;
1303
+ return !hookEntry.hooks?.some((h) => h.command?.includes(CLAUDE_HOOK_MARKER));
1304
+ });
1305
+ return hooks.SessionStart.length < originalLength;
1306
+ }
1307
+ async function writeClaudeSettings(settingsPath, settings) {
1308
+ const dir = dirname(settingsPath);
1309
+ await mkdir(dir, { recursive: true });
1310
+ const tempPath = settingsPath + ".tmp";
1311
+ await writeFile(tempPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1312
+ await rename(tempPath, settingsPath);
1313
+ }
1314
+ var setupCommand = program.command("setup").description("Setup integrations");
1315
+ setupCommand.command("claude").description("Install Claude Code SessionStart hooks").option("--project", "Install to project .claude directory instead of global").option("--uninstall", "Remove learning-agent hooks").option("--dry-run", "Show what would change without writing").option("--json", "Output as JSON").action(async (options) => {
1316
+ const settingsPath = getClaudeSettingsPath(options.project ?? false);
1317
+ const displayPath = options.project ? ".claude/settings.json" : "~/.claude/settings.json";
1318
+ let settings;
1319
+ try {
1320
+ settings = await readClaudeSettings(settingsPath);
1321
+ } catch {
1322
+ if (options.json) {
1323
+ console.log(JSON.stringify({ error: "Failed to parse settings file" }));
1324
+ } else {
1325
+ out.error("Failed to parse settings file. Check if JSON is valid.");
1326
+ }
1327
+ process.exit(1);
1328
+ }
1329
+ const alreadyInstalled = hasClaudeHook(settings);
1330
+ if (options.uninstall) {
1331
+ if (options.dryRun) {
1332
+ if (options.json) {
1333
+ console.log(JSON.stringify({ dryRun: true, wouldRemove: alreadyInstalled, location: displayPath }));
1334
+ } else {
1335
+ if (alreadyInstalled) {
1336
+ console.log(`Would remove learning-agent hooks from ${displayPath}`);
1337
+ } else {
1338
+ console.log("No learning-agent hooks to remove");
1339
+ }
1340
+ }
1341
+ return;
1342
+ }
1343
+ const removed = removeLearningAgentHook(settings);
1344
+ if (removed) {
1345
+ await writeClaudeSettings(settingsPath, settings);
1346
+ if (options.json) {
1347
+ console.log(JSON.stringify({ installed: false, location: displayPath, action: "removed" }));
1348
+ } else {
1349
+ out.success("Learning agent hooks removed");
1350
+ console.log(` Location: ${displayPath}`);
1351
+ }
1352
+ } else {
1353
+ if (options.json) {
1354
+ console.log(JSON.stringify({ installed: false, location: displayPath, action: "unchanged" }));
1355
+ } else {
1356
+ out.info("No learning agent hooks to remove");
1357
+ }
1358
+ }
1359
+ return;
1360
+ }
1361
+ if (options.dryRun) {
1362
+ if (options.json) {
1363
+ console.log(JSON.stringify({ dryRun: true, wouldInstall: !alreadyInstalled, location: displayPath }));
1364
+ } else {
1365
+ if (alreadyInstalled) {
1366
+ console.log("Learning agent hooks already installed");
1367
+ } else {
1368
+ console.log(`Would install learning-agent hooks to ${displayPath}`);
1369
+ }
1370
+ }
1371
+ return;
1372
+ }
1373
+ if (alreadyInstalled) {
1374
+ if (options.json) {
1375
+ console.log(JSON.stringify({
1376
+ installed: true,
1377
+ location: displayPath,
1378
+ hooks: ["SessionStart"],
1379
+ action: "unchanged"
1380
+ }));
1381
+ } else {
1382
+ out.info("Learning agent hooks already installed");
1383
+ console.log(` Location: ${displayPath}`);
1384
+ }
1385
+ return;
1386
+ }
1387
+ const fileExists = existsSync(settingsPath);
1388
+ addLearningAgentHook(settings);
1389
+ await writeClaudeSettings(settingsPath, settings);
1390
+ if (options.json) {
1391
+ console.log(JSON.stringify({
1392
+ installed: true,
1393
+ location: displayPath,
1394
+ hooks: ["SessionStart"],
1395
+ action: fileExists ? "updated" : "created"
1396
+ }));
1397
+ } else {
1398
+ out.success(options.project ? "Claude Code hooks installed (project-level)" : "Claude Code hooks installed");
1399
+ console.log(` Location: ${displayPath}`);
1400
+ console.log(" Hook: SessionStart (startup|resume|compact)");
1401
+ console.log("");
1402
+ console.log("Lessons will be loaded automatically at session start.");
1403
+ if (options.project) {
1404
+ console.log("");
1405
+ console.log("Note: Project hooks override global hooks.");
1406
+ }
1407
+ }
1408
+ });
774
1409
  program.command("learn <insight>").description("Capture a new lesson").option("-t, --trigger <text>", "What triggered this lesson").option("--tags <tags>", "Comma-separated tags", "").option("-y, --yes", "Skip confirmation").action(async function(insight, options) {
775
1410
  const repoRoot = getRepoRoot();
776
1411
  const { quiet } = getGlobalOpts(this);
@@ -786,7 +1421,8 @@ program.command("learn <insight>").description("Capture a new lesson").option("-
786
1421
  intent: "manual learning"
787
1422
  },
788
1423
  created: (/* @__PURE__ */ new Date()).toISOString(),
789
- confirmed: options.yes ?? false,
1424
+ confirmed: true,
1425
+ // learn command is explicit confirmation
790
1426
  supersedes: [],
791
1427
  related: []
792
1428
  };
@@ -875,9 +1511,18 @@ program.command("rebuild").description("Rebuild SQLite index from JSONL").option
875
1511
  }
876
1512
  }
877
1513
  });
878
- program.command("detect").description("Detect learning triggers from input").requiredOption("--input <file>", "Path to JSON input file").option("--save", "Automatically save proposed lesson").option("--json", "Output result as JSON").action(
1514
+ program.command("detect").description("Detect learning triggers from input").requiredOption("--input <file>", "Path to JSON input file").option("--save", "Save proposed lesson (requires --yes)").option("-y, --yes", "Confirm save (required with --save)").option("--json", "Output result as JSON").action(
879
1515
  async (options) => {
880
1516
  const repoRoot = getRepoRoot();
1517
+ if (options.save && !options.yes) {
1518
+ if (options.json) {
1519
+ console.log(JSON.stringify({ error: "--save requires --yes flag for confirmation" }));
1520
+ } else {
1521
+ out.error("--save requires --yes flag for confirmation");
1522
+ console.log("Use: detect --input <file> --save --yes");
1523
+ }
1524
+ process.exit(1);
1525
+ }
881
1526
  const input = await parseInputFile(options.input);
882
1527
  const result = await detectAndPropose(repoRoot, input);
883
1528
  if (!result) {
@@ -896,7 +1541,7 @@ program.command("detect").description("Detect learning triggers from input").req
896
1541
  console.log(` Trigger: ${result.trigger}`);
897
1542
  console.log(` Source: ${result.source}`);
898
1543
  console.log(` Proposed: ${result.proposedInsight}`);
899
- if (options.save) {
1544
+ if (options.save && options.yes) {
900
1545
  const lesson = {
901
1546
  id: generateId(result.proposedInsight),
902
1547
  type: "quick",
@@ -906,7 +1551,8 @@ program.command("detect").description("Detect learning triggers from input").req
906
1551
  source: result.source,
907
1552
  context: { tool: "detect", intent: "auto-capture" },
908
1553
  created: (/* @__PURE__ */ new Date()).toISOString(),
909
- confirmed: false,
1554
+ confirmed: true,
1555
+ // --yes confirms the lesson
910
1556
  supersedes: [],
911
1557
  related: []
912
1558
  };
@@ -916,6 +1562,45 @@ Saved as lesson: ${lesson.id}`);
916
1562
  }
917
1563
  }
918
1564
  );
1565
+ program.command("capture").description("Capture a lesson from trigger/insight or input file").option("-t, --trigger <text>", "What triggered this lesson").option("-i, --insight <text>", "The insight or lesson learned").option("--input <file>", "Path to JSON input file (alternative to trigger/insight)").option("--json", "Output result as JSON").option("-y, --yes", "Skip confirmation and save immediately").action(async function(options) {
1566
+ const repoRoot = getRepoRoot();
1567
+ const { verbose } = getGlobalOpts(this);
1568
+ let lesson;
1569
+ if (options.input) {
1570
+ const input = await parseInputFile(options.input);
1571
+ const result = await detectAndPropose(repoRoot, input);
1572
+ if (!result) {
1573
+ options.json ? console.log(JSON.stringify({ detected: false, saved: false })) : console.log("No learning trigger detected.");
1574
+ return;
1575
+ }
1576
+ lesson = createLessonFromInputFile(result, options.yes ?? false);
1577
+ } else if (options.trigger && options.insight) {
1578
+ lesson = createLessonFromFlags(options.trigger, options.insight, options.yes ?? false);
1579
+ } else {
1580
+ const msg = "Provide either --trigger and --insight, or --input file.";
1581
+ options.json ? console.log(JSON.stringify({ error: msg, saved: false })) : out.error(msg);
1582
+ process.exit(1);
1583
+ }
1584
+ if (!options.yes && !process.stdin.isTTY) {
1585
+ if (options.json) {
1586
+ console.log(JSON.stringify({ error: "--yes required in non-interactive mode", saved: false }));
1587
+ } else {
1588
+ out.error("--yes required in non-interactive mode");
1589
+ console.log('Use: capture --trigger "..." --insight "..." --yes');
1590
+ }
1591
+ process.exit(1);
1592
+ }
1593
+ if (options.json) {
1594
+ if (options.yes) await appendLesson(repoRoot, lesson);
1595
+ outputCaptureJson(lesson, options.yes ?? false);
1596
+ } else if (options.yes) {
1597
+ await appendLesson(repoRoot, lesson);
1598
+ out.success(`Lesson saved: ${lesson.id}`);
1599
+ if (verbose) console.log(` Type: ${lesson.type} | Trigger: ${lesson.trigger}`);
1600
+ } else {
1601
+ outputCapturePreview(lesson);
1602
+ }
1603
+ });
919
1604
  program.command("compact").description("Compact lessons: archive old lessons and remove tombstones").option("-f, --force", "Run compaction even if below threshold").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
920
1605
  const repoRoot = getRepoRoot();
921
1606
  const tombstones = await countTombstones(repoRoot);
@@ -956,14 +1641,14 @@ program.command("export").description("Export lessons as JSON to stdout").option
956
1641
  const filterTags = options.tags.split(",").map((t) => t.trim());
957
1642
  filtered = filtered.filter((lesson) => lesson.tags.some((tag) => filterTags.includes(tag)));
958
1643
  }
959
- console.log(JSON.stringify(filtered, null, 2));
1644
+ console.log(JSON.stringify(filtered, null, JSON_INDENT_SPACES));
960
1645
  });
961
1646
  program.command("import <file>").description("Import lessons from a JSONL file").action(async (file) => {
962
1647
  const repoRoot = getRepoRoot();
963
1648
  let content;
964
1649
  try {
965
- const { readFile: readFile4 } = await import('fs/promises');
966
- content = await readFile4(file, "utf-8");
1650
+ const { readFile: readFile5 } = await import('fs/promises');
1651
+ content = await readFile5(file, "utf-8");
967
1652
  } catch (err) {
968
1653
  const code = err.code;
969
1654
  if (code === "ENOENT") {
@@ -1021,7 +1706,7 @@ program.command("stats").description("Show database health and statistics").acti
1021
1706
  const totalLessons = lessons.length;
1022
1707
  const retrievalStats = getRetrievalStats(repoRoot);
1023
1708
  const totalRetrievals = retrievalStats.reduce((sum, s) => sum + s.count, 0);
1024
- const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(1) : "0.0";
1709
+ const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(AVG_DECIMAL_PLACES) : "0.0";
1025
1710
  const jsonlPath = join(repoRoot, LESSONS_PATH);
1026
1711
  const dbPath = join(repoRoot, DB_PATH);
1027
1712
  let dataSize = 0;
@@ -1040,6 +1725,63 @@ program.command("stats").description("Show database health and statistics").acti
1040
1725
  console.log(`Retrievals: ${totalRetrievals} total, ${avgRetrievals} avg per lesson`);
1041
1726
  console.log(`Storage: ${formatBytes(totalSize)} (index: ${formatBytes(indexSize)}, data: ${formatBytes(dataSize)})`);
1042
1727
  });
1728
+ program.command("load-session").description("Load high-severity lessons for session context").option("--json", "Output as JSON").action(async function(options) {
1729
+ const repoRoot = getRepoRoot();
1730
+ const { quiet } = getGlobalOpts(this);
1731
+ const lessons = await loadSessionLessons(repoRoot);
1732
+ if (options.json) {
1733
+ console.log(JSON.stringify({ lessons, count: lessons.length }));
1734
+ return;
1735
+ }
1736
+ if (lessons.length === 0) {
1737
+ console.log("No high-severity lessons found.");
1738
+ return;
1739
+ }
1740
+ outputSessionLessonsHuman(lessons, quiet);
1741
+ });
1742
+ program.command("check-plan").description("Check plan against relevant lessons").option("--plan <text>", "Plan text to check").option("--json", "Output as JSON").option("-n, --limit <number>", "Maximum results", DEFAULT_CHECK_PLAN_LIMIT).action(async function(options) {
1743
+ const repoRoot = getRepoRoot();
1744
+ const limit = parseLimit(options.limit, "limit");
1745
+ const { quiet } = getGlobalOpts(this);
1746
+ const planText = options.plan ?? await readPlanFromStdin();
1747
+ if (!planText) {
1748
+ out.error("No plan provided. Use --plan <text> or pipe text to stdin.");
1749
+ process.exit(1);
1750
+ }
1751
+ if (!isModelAvailable()) {
1752
+ if (options.json) {
1753
+ console.log(JSON.stringify({
1754
+ error: "Embedding model not available",
1755
+ action: "Run: npx learning-agent download-model"
1756
+ }));
1757
+ } else {
1758
+ out.error("Embedding model not available");
1759
+ console.log("");
1760
+ console.log("Run: npx learning-agent download-model");
1761
+ }
1762
+ process.exit(1);
1763
+ }
1764
+ try {
1765
+ const result = await retrieveForPlan(repoRoot, planText, limit);
1766
+ if (options.json) {
1767
+ outputCheckPlanJson(result.lessons);
1768
+ return;
1769
+ }
1770
+ if (result.lessons.length === 0) {
1771
+ console.log("No relevant lessons found for this plan.");
1772
+ return;
1773
+ }
1774
+ outputCheckPlanHuman(result.lessons, quiet);
1775
+ } catch (err) {
1776
+ const message = err instanceof Error ? err.message : "Unknown error";
1777
+ if (options.json) {
1778
+ console.log(JSON.stringify({ error: message }));
1779
+ } else {
1780
+ out.error(`Failed to check plan: ${message}`);
1781
+ }
1782
+ process.exit(1);
1783
+ }
1784
+ });
1043
1785
  program.parse();
1044
1786
  //# sourceMappingURL=cli.js.map
1045
1787
  //# sourceMappingURL=cli.js.map