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/CHANGELOG.md +21 -2
- package/README.md +34 -5
- package/dist/cli.js +758 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/package.json +10 -19
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,
|
|
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
|
-
|
|
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:
|
|
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", "
|
|
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:
|
|
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,
|
|
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:
|
|
966
|
-
content = await
|
|
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(
|
|
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
|