athar-mcp 0.1.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/LICENSE +21 -0
- package/README.md +242 -0
- package/dist/cli/commands/list.d.ts +10 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +73 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/review.d.ts +2 -0
- package/dist/cli/commands/review.d.ts.map +1 -0
- package/dist/cli/commands/review.js +112 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +2 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +55 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +39 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/index.d.ts +12 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +43 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/db/connection.d.ts +11 -0
- package/dist/db/connection.d.ts.map +1 -0
- package/dist/db/connection.js +49 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/schema.d.ts +12 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +95 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +139 -0
- package/dist/server.js.map +1 -0
- package/dist/smoke-test.d.ts +7 -0
- package/dist/smoke-test.d.ts.map +1 -0
- package/dist/smoke-test.js +99 -0
- package/dist/smoke-test.js.map +1 -0
- package/dist/spaced-repetition/scheduler.d.ts +46 -0
- package/dist/spaced-repetition/scheduler.d.ts.map +1 -0
- package/dist/spaced-repetition/scheduler.js +98 -0
- package/dist/spaced-repetition/scheduler.js.map +1 -0
- package/dist/spaced-repetition/sm2.d.ts +36 -0
- package/dist/spaced-repetition/sm2.d.ts.map +1 -0
- package/dist/spaced-repetition/sm2.js +90 -0
- package/dist/spaced-repetition/sm2.js.map +1 -0
- package/dist/tools/memory.d.ts +25 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +112 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/save-lesson.d.ts +11 -0
- package/dist/tools/save-lesson.d.ts.map +1 -0
- package/dist/tools/save-lesson.js +137 -0
- package/dist/tools/save-lesson.js.map +1 -0
- package/dist/tools/validators.d.ts +82 -0
- package/dist/tools/validators.d.ts.map +1 -0
- package/dist/tools/validators.js +86 -0
- package/dist/tools/validators.js.map +1 -0
- package/dist/utils/logger.d.ts +23 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +55 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/paths.d.ts +17 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +43 -0
- package/dist/utils/paths.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { getDatabase } from '../db/connection.js';
|
|
2
|
+
import { calculateSM2 } from './sm2.js';
|
|
3
|
+
import { createLogger } from '../utils/logger.js';
|
|
4
|
+
const log = createLogger('scheduler');
|
|
5
|
+
/**
|
|
6
|
+
* Get all lessons that are due for review (next_review_at <= now).
|
|
7
|
+
*/
|
|
8
|
+
export function getDueReviews() {
|
|
9
|
+
const db = getDatabase();
|
|
10
|
+
const now = new Date().toISOString();
|
|
11
|
+
const stmt = db.prepare(`
|
|
12
|
+
SELECT id, title, problem, root_cause, lesson,
|
|
13
|
+
bad_code, good_code, tags, language,
|
|
14
|
+
review_questions, repetitions, easiness_factor,
|
|
15
|
+
interval_days, status, review_count, created_at
|
|
16
|
+
FROM lessons
|
|
17
|
+
WHERE next_review_at <= ? OR next_review_at IS NULL
|
|
18
|
+
ORDER BY next_review_at ASC
|
|
19
|
+
`);
|
|
20
|
+
return stmt.all(now);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Update a lesson after a review session.
|
|
24
|
+
* Applies the SM-2 algorithm and updates the database.
|
|
25
|
+
*/
|
|
26
|
+
export function recordReview(lessonId, quality) {
|
|
27
|
+
const db = getDatabase();
|
|
28
|
+
// Get current state
|
|
29
|
+
const lesson = db.prepare(`
|
|
30
|
+
SELECT repetitions, easiness_factor, interval_days
|
|
31
|
+
FROM lessons WHERE id = ?
|
|
32
|
+
`).get(lessonId);
|
|
33
|
+
if (!lesson) {
|
|
34
|
+
throw new Error(`Lesson with ID ${lessonId} not found`);
|
|
35
|
+
}
|
|
36
|
+
// Calculate SM-2
|
|
37
|
+
const result = calculateSM2({
|
|
38
|
+
repetitions: lesson.repetitions,
|
|
39
|
+
easinessFactor: lesson.easiness_factor,
|
|
40
|
+
intervalDays: lesson.interval_days,
|
|
41
|
+
}, quality);
|
|
42
|
+
// Update database
|
|
43
|
+
db.prepare(`
|
|
44
|
+
UPDATE lessons SET
|
|
45
|
+
repetitions = ?,
|
|
46
|
+
easiness_factor = ?,
|
|
47
|
+
interval_days = ?,
|
|
48
|
+
next_review_at = ?,
|
|
49
|
+
last_reviewed_at = datetime('now'),
|
|
50
|
+
status = ?,
|
|
51
|
+
quality_score = ?,
|
|
52
|
+
review_count = review_count + 1,
|
|
53
|
+
updated_at = datetime('now')
|
|
54
|
+
WHERE id = ?
|
|
55
|
+
`).run(result.repetitions, result.easinessFactor, result.intervalDays, result.nextReviewAt.toISOString(), result.status, quality, lessonId);
|
|
56
|
+
log.info(`Review recorded: lesson=${lessonId}, quality=${quality}, next_review=${result.intervalDays}d, status=${result.status}`);
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
nextReviewAt: result.nextReviewAt,
|
|
60
|
+
newStatus: result.status,
|
|
61
|
+
intervalDays: result.intervalDays,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get statistics about all lessons.
|
|
66
|
+
*/
|
|
67
|
+
export function getStats() {
|
|
68
|
+
const db = getDatabase();
|
|
69
|
+
const now = new Date().toISOString();
|
|
70
|
+
const weekFromNow = new Date();
|
|
71
|
+
weekFromNow.setDate(weekFromNow.getDate() + 7);
|
|
72
|
+
const total = db.prepare('SELECT COUNT(*) as c FROM lessons').get().c;
|
|
73
|
+
const dueToday = db.prepare('SELECT COUNT(*) as c FROM lessons WHERE next_review_at <= ? OR next_review_at IS NULL').get(now).c;
|
|
74
|
+
const dueThisWeek = db.prepare('SELECT COUNT(*) as c FROM lessons WHERE next_review_at <= ?').get(weekFromNow.toISOString()).c;
|
|
75
|
+
const statusRows = db.prepare('SELECT status, COUNT(*) as c FROM lessons GROUP BY status').all();
|
|
76
|
+
const byStatus = {};
|
|
77
|
+
for (const row of statusRows) {
|
|
78
|
+
byStatus[row.status] = row.c;
|
|
79
|
+
}
|
|
80
|
+
// Extract and count tags
|
|
81
|
+
const allTags = db.prepare('SELECT tags FROM lessons').all();
|
|
82
|
+
const tagCounts = new Map();
|
|
83
|
+
for (const row of allTags) {
|
|
84
|
+
try {
|
|
85
|
+
const tags = JSON.parse(row.tags || '[]');
|
|
86
|
+
for (const tag of tags) {
|
|
87
|
+
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch { /* skip malformed */ }
|
|
91
|
+
}
|
|
92
|
+
const topTags = [...tagCounts.entries()]
|
|
93
|
+
.sort((a, b) => b[1] - a[1])
|
|
94
|
+
.slice(0, 8)
|
|
95
|
+
.map(([tag, count]) => ({ tag, count }));
|
|
96
|
+
return { total, dueToday, dueThisWeek, byStatus, topTags };
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/spaced-repetition/scheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SM-2 Spaced Repetition Algorithm
|
|
3
|
+
*
|
|
4
|
+
* Based on the SuperMemo-2 algorithm by Piotr Wozniak.
|
|
5
|
+
* Calculates optimal review intervals based on recall quality.
|
|
6
|
+
*
|
|
7
|
+
* Quality scores:
|
|
8
|
+
* 5 ā Perfect response (instant recall)
|
|
9
|
+
* 4 ā Correct after hesitation
|
|
10
|
+
* 3 ā Correct with significant difficulty
|
|
11
|
+
* 2 ā Incorrect, but felt close / recognized the answer
|
|
12
|
+
* 1 ā Incorrect, vague memory
|
|
13
|
+
* 0 ā Complete blackout
|
|
14
|
+
*/
|
|
15
|
+
export interface SM2State {
|
|
16
|
+
repetitions: number;
|
|
17
|
+
easinessFactor: number;
|
|
18
|
+
intervalDays: number;
|
|
19
|
+
}
|
|
20
|
+
export interface SM2Result extends SM2State {
|
|
21
|
+
nextReviewAt: Date;
|
|
22
|
+
status: 'new' | 'learning' | 'learned' | 'mastered';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Calculate the next review state using the SM-2 algorithm.
|
|
26
|
+
*
|
|
27
|
+
* @param current - Current card state
|
|
28
|
+
* @param quality - Recall quality (0-5)
|
|
29
|
+
* @returns Updated state with next review date
|
|
30
|
+
*/
|
|
31
|
+
export declare function calculateSM2(current: SM2State, quality: number): SM2Result;
|
|
32
|
+
/**
|
|
33
|
+
* Get a human-readable description of the quality score.
|
|
34
|
+
*/
|
|
35
|
+
export declare function qualityLabel(q: number): string;
|
|
36
|
+
//# sourceMappingURL=sm2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sm2.d.ts","sourceRoot":"","sources":["../../src/spaced-repetition/sm2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,QAAQ;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAU,SAAQ,QAAQ;IACzC,YAAY,EAAE,IAAI,CAAC;IACnB,MAAM,EAAE,KAAK,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,CAAC;CACrD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAsD1E;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAU9C"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SM-2 Spaced Repetition Algorithm
|
|
3
|
+
*
|
|
4
|
+
* Based on the SuperMemo-2 algorithm by Piotr Wozniak.
|
|
5
|
+
* Calculates optimal review intervals based on recall quality.
|
|
6
|
+
*
|
|
7
|
+
* Quality scores:
|
|
8
|
+
* 5 ā Perfect response (instant recall)
|
|
9
|
+
* 4 ā Correct after hesitation
|
|
10
|
+
* 3 ā Correct with significant difficulty
|
|
11
|
+
* 2 ā Incorrect, but felt close / recognized the answer
|
|
12
|
+
* 1 ā Incorrect, vague memory
|
|
13
|
+
* 0 ā Complete blackout
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Calculate the next review state using the SM-2 algorithm.
|
|
17
|
+
*
|
|
18
|
+
* @param current - Current card state
|
|
19
|
+
* @param quality - Recall quality (0-5)
|
|
20
|
+
* @returns Updated state with next review date
|
|
21
|
+
*/
|
|
22
|
+
export function calculateSM2(current, quality) {
|
|
23
|
+
// Clamp quality to valid range
|
|
24
|
+
quality = Math.max(0, Math.min(5, Math.round(quality)));
|
|
25
|
+
let { repetitions, easinessFactor, intervalDays } = current;
|
|
26
|
+
// Calculate new Easiness Factor
|
|
27
|
+
// EF' = EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))
|
|
28
|
+
let newEF = easinessFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
|
|
29
|
+
if (newEF < 1.3)
|
|
30
|
+
newEF = 1.3; // EF cannot drop below 1.3
|
|
31
|
+
let newRepetitions;
|
|
32
|
+
let newInterval;
|
|
33
|
+
if (quality < 3) {
|
|
34
|
+
// Failed recall ā reset to beginning
|
|
35
|
+
newRepetitions = 0;
|
|
36
|
+
newInterval = 1; // Review again tomorrow
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Successful recall ā advance
|
|
40
|
+
newRepetitions = repetitions + 1;
|
|
41
|
+
if (newRepetitions === 1) {
|
|
42
|
+
newInterval = 1; // First success: review in 1 day
|
|
43
|
+
}
|
|
44
|
+
else if (newRepetitions === 2) {
|
|
45
|
+
newInterval = 6; // Second success: review in 6 days
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
newInterval = Math.round(intervalDays * newEF);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Calculate next review date
|
|
52
|
+
const nextReviewAt = new Date();
|
|
53
|
+
nextReviewAt.setDate(nextReviewAt.getDate() + newInterval);
|
|
54
|
+
// Determine status based on repetitions and EF
|
|
55
|
+
let status;
|
|
56
|
+
if (newRepetitions === 0) {
|
|
57
|
+
status = 'new';
|
|
58
|
+
}
|
|
59
|
+
else if (newRepetitions <= 2) {
|
|
60
|
+
status = 'learning';
|
|
61
|
+
}
|
|
62
|
+
else if (newRepetitions <= 5 || newEF < 2.5) {
|
|
63
|
+
status = 'learned';
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
status = 'mastered';
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
repetitions: newRepetitions,
|
|
70
|
+
easinessFactor: Math.round(newEF * 100) / 100, // Round to 2 decimals
|
|
71
|
+
intervalDays: newInterval,
|
|
72
|
+
nextReviewAt,
|
|
73
|
+
status,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get a human-readable description of the quality score.
|
|
78
|
+
*/
|
|
79
|
+
export function qualityLabel(q) {
|
|
80
|
+
switch (q) {
|
|
81
|
+
case 0: return 'š„ Complete blackout';
|
|
82
|
+
case 1: return 'š§ Incorrect, vague memory';
|
|
83
|
+
case 2: return 'šØ Incorrect, but recognized the answer';
|
|
84
|
+
case 3: return 'š© Correct with significant difficulty';
|
|
85
|
+
case 4: return 'š¦ Correct after hesitation';
|
|
86
|
+
case 5: return 'šŖ Perfect recall';
|
|
87
|
+
default: return `Unknown (${q})`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=sm2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sm2.js","sourceRoot":"","sources":["../../src/spaced-repetition/sm2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAaH;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,OAAiB,EAAE,OAAe;IAC7D,+BAA+B;IAC/B,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAExD,IAAI,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC;IAE5D,gCAAgC;IAChC,uDAAuD;IACvD,IAAI,KAAK,GAAG,cAAc,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IACnF,IAAI,KAAK,GAAG,GAAG;QAAE,KAAK,GAAG,GAAG,CAAC,CAAC,2BAA2B;IAEzD,IAAI,cAAsB,CAAC;IAC3B,IAAI,WAAmB,CAAC;IAExB,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAChB,qCAAqC;QACrC,cAAc,GAAG,CAAC,CAAC;QACnB,WAAW,GAAG,CAAC,CAAC,CAAC,wBAAwB;IAC3C,CAAC;SAAM,CAAC;QACN,8BAA8B;QAC9B,cAAc,GAAG,WAAW,GAAG,CAAC,CAAC;QAEjC,IAAI,cAAc,KAAK,CAAC,EAAE,CAAC;YACzB,WAAW,GAAG,CAAC,CAAC,CAAS,iCAAiC;QAC5D,CAAC;aAAM,IAAI,cAAc,KAAK,CAAC,EAAE,CAAC;YAChC,WAAW,GAAG,CAAC,CAAC,CAAS,mCAAmC;QAC9D,CAAC;aAAM,CAAC;YACN,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IAED,6BAA6B;IAC7B,MAAM,YAAY,GAAG,IAAI,IAAI,EAAE,CAAC;IAChC,YAAY,CAAC,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,GAAG,WAAW,CAAC,CAAC;IAE3D,+CAA+C;IAC/C,IAAI,MAA2B,CAAC;IAChC,IAAI,cAAc,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,GAAG,KAAK,CAAC;IACjB,CAAC;SAAM,IAAI,cAAc,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,GAAG,UAAU,CAAC;IACtB,CAAC;SAAM,IAAI,cAAc,IAAI,CAAC,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;QAC9C,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,UAAU,CAAC;IACtB,CAAC;IAED,OAAO;QACL,WAAW,EAAE,cAAc;QAC3B,cAAc,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,EAAE,sBAAsB;QACrE,YAAY,EAAE,WAAW;QACzB,YAAY;QACZ,MAAM;KACP,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,CAAS;IACpC,QAAQ,CAAC,EAAE,CAAC;QACV,KAAK,CAAC,CAAC,CAAC,OAAO,sBAAsB,CAAC;QACtC,KAAK,CAAC,CAAC,CAAC,OAAO,4BAA4B,CAAC;QAC5C,KAAK,CAAC,CAAC,CAAC,OAAO,yCAAyC,CAAC;QACzD,KAAK,CAAC,CAAC,CAAC,OAAO,wCAAwC,CAAC;QACxD,KAAK,CAAC,CAAC,CAAC,OAAO,6BAA6B,CAAC;QAC7C,KAAK,CAAC,CAAC,CAAC,OAAO,mBAAmB,CAAC;QACnC,OAAO,CAAC,CAAC,OAAO,YAAY,CAAC,GAAG,CAAC;IACnC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MemoryInput } from './validators.js';
|
|
2
|
+
interface LessonResult {
|
|
3
|
+
id: number;
|
|
4
|
+
title: string;
|
|
5
|
+
problem: string;
|
|
6
|
+
root_cause: string;
|
|
7
|
+
lesson: string;
|
|
8
|
+
bad_code: string | null;
|
|
9
|
+
good_code: string | null;
|
|
10
|
+
tags: string;
|
|
11
|
+
language: string | null;
|
|
12
|
+
status: string;
|
|
13
|
+
review_count: number;
|
|
14
|
+
created_at: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Search and retrieve past lessons from memory.
|
|
18
|
+
* Called by the MCP memory tool handler.
|
|
19
|
+
*/
|
|
20
|
+
export declare function searchMemory(input: MemoryInput): {
|
|
21
|
+
results: LessonResult[];
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/tools/memory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD,UAAU,YAAY;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG;IAAE,OAAO,EAAE,YAAY,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA4H7F"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { getDatabase } from '../db/connection.js';
|
|
2
|
+
import { createLogger } from '../utils/logger.js';
|
|
3
|
+
const log = createLogger('memory');
|
|
4
|
+
/**
|
|
5
|
+
* Search and retrieve past lessons from memory.
|
|
6
|
+
* Called by the MCP memory tool handler.
|
|
7
|
+
*/
|
|
8
|
+
export function searchMemory(input) {
|
|
9
|
+
const db = getDatabase();
|
|
10
|
+
log.info(`Searching memory: query="${input.query}", limit=${input.limit}`);
|
|
11
|
+
let results = [];
|
|
12
|
+
// Strategy 1: Try FTS5 full-text search first
|
|
13
|
+
try {
|
|
14
|
+
const ftsQuery = input.query
|
|
15
|
+
.replace(/[^\w\s]/g, ' ') // Remove special chars
|
|
16
|
+
.split(/\s+/)
|
|
17
|
+
.filter(w => w.length > 1)
|
|
18
|
+
.map(w => `"${w}"`) // Exact phrase matching per word
|
|
19
|
+
.join(' OR ');
|
|
20
|
+
if (ftsQuery) {
|
|
21
|
+
let sql = `
|
|
22
|
+
SELECT l.id, l.title, l.problem, l.root_cause, l.lesson,
|
|
23
|
+
l.bad_code, l.good_code, l.tags, l.language,
|
|
24
|
+
l.status, l.review_count, l.created_at
|
|
25
|
+
FROM lessons_fts fts
|
|
26
|
+
JOIN lessons l ON l.id = fts.rowid
|
|
27
|
+
`;
|
|
28
|
+
const conditions = [];
|
|
29
|
+
const params = [];
|
|
30
|
+
conditions.push('lessons_fts MATCH ?');
|
|
31
|
+
params.push(ftsQuery);
|
|
32
|
+
if (input.language) {
|
|
33
|
+
conditions.push('l.language = ?');
|
|
34
|
+
params.push(input.language);
|
|
35
|
+
}
|
|
36
|
+
if (input.tags && input.tags.length > 0) {
|
|
37
|
+
// Match any tag using JSON
|
|
38
|
+
const tagConditions = input.tags.map(() => 'l.tags LIKE ?');
|
|
39
|
+
conditions.push(`(${tagConditions.join(' OR ')})`);
|
|
40
|
+
params.push(...input.tags.map(t => `%"${t}"%`));
|
|
41
|
+
}
|
|
42
|
+
sql += ' WHERE ' + conditions.join(' AND ');
|
|
43
|
+
sql += ` ORDER BY rank LIMIT ?`;
|
|
44
|
+
params.push(input.limit);
|
|
45
|
+
const stmt = db.prepare(sql);
|
|
46
|
+
results = stmt.all(...params);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
log.debug('FTS search failed, falling back to LIKE search', String(err));
|
|
51
|
+
}
|
|
52
|
+
// Strategy 2: Fallback to LIKE search if FTS returns nothing
|
|
53
|
+
if (results.length === 0) {
|
|
54
|
+
let sql = `
|
|
55
|
+
SELECT id, title, problem, root_cause, lesson,
|
|
56
|
+
bad_code, good_code, tags, language,
|
|
57
|
+
status, review_count, created_at
|
|
58
|
+
FROM lessons
|
|
59
|
+
WHERE (
|
|
60
|
+
title LIKE ? OR
|
|
61
|
+
problem LIKE ? OR
|
|
62
|
+
root_cause LIKE ? OR
|
|
63
|
+
lesson LIKE ? OR
|
|
64
|
+
tags LIKE ?
|
|
65
|
+
)
|
|
66
|
+
`;
|
|
67
|
+
const likeQuery = `%${input.query}%`;
|
|
68
|
+
const params = [likeQuery, likeQuery, likeQuery, likeQuery, likeQuery];
|
|
69
|
+
if (input.language) {
|
|
70
|
+
sql += ' AND language = ?';
|
|
71
|
+
params.push(input.language);
|
|
72
|
+
}
|
|
73
|
+
if (input.tags && input.tags.length > 0) {
|
|
74
|
+
const tagConditions = input.tags.map(() => 'tags LIKE ?');
|
|
75
|
+
sql += ` AND (${tagConditions.join(' OR ')})`;
|
|
76
|
+
params.push(...input.tags.map(t => `%"${t}"%`));
|
|
77
|
+
}
|
|
78
|
+
sql += ` ORDER BY created_at DESC LIMIT ?`;
|
|
79
|
+
params.push(input.limit);
|
|
80
|
+
const stmt = db.prepare(sql);
|
|
81
|
+
results = stmt.all(...params);
|
|
82
|
+
}
|
|
83
|
+
log.info(`Found ${results.length} matching lessons`);
|
|
84
|
+
if (results.length === 0) {
|
|
85
|
+
return {
|
|
86
|
+
results: [],
|
|
87
|
+
message: `No lessons found matching "${input.query}". This might be a new type of mistake ā if resolved, consider saving it as a lesson.`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Format results for the AI assistant
|
|
91
|
+
const formatted = results.map((r, i) => {
|
|
92
|
+
const tags = JSON.parse(r.tags || '[]');
|
|
93
|
+
let text = `\n### ${i + 1}. ${r.title} (ID: ${r.id})\n`;
|
|
94
|
+
text += `**Status:** ${r.status} | **Reviews:** ${r.review_count} | **Language:** ${r.language || 'N/A'}\n`;
|
|
95
|
+
text += `**Tags:** ${tags.join(', ')}\n\n`;
|
|
96
|
+
text += `**Problem:** ${r.problem}\n\n`;
|
|
97
|
+
text += `**Root Cause:** ${r.root_cause}\n\n`;
|
|
98
|
+
text += `**Lesson:** ${r.lesson}\n`;
|
|
99
|
+
if (r.bad_code) {
|
|
100
|
+
text += `\n**Bad Code:**\n\`\`\`\n${r.bad_code}\n\`\`\`\n`;
|
|
101
|
+
}
|
|
102
|
+
if (r.good_code) {
|
|
103
|
+
text += `\n**Good Code:**\n\`\`\`\n${r.good_code}\n\`\`\`\n`;
|
|
104
|
+
}
|
|
105
|
+
return text;
|
|
106
|
+
}).join('\n---\n');
|
|
107
|
+
return {
|
|
108
|
+
results,
|
|
109
|
+
message: `Found ${results.length} relevant lesson(s) from memory:\n${formatted}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/tools/memory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SaveLessonInput } from './validators.js';
|
|
2
|
+
/**
|
|
3
|
+
* Save a new programming lesson to the database.
|
|
4
|
+
* Called by the MCP save_lesson tool handler.
|
|
5
|
+
*/
|
|
6
|
+
export declare function saveLesson(input: SaveLessonInput): {
|
|
7
|
+
success: boolean;
|
|
8
|
+
message: string;
|
|
9
|
+
lessonId?: number;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=save-lesson.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"save-lesson.d.ts","sourceRoot":"","sources":["../../src/tools/save-lesson.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAuGvD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CA8D3G"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { getDatabase } from '../db/connection.js';
|
|
2
|
+
import { createLogger } from '../utils/logger.js';
|
|
3
|
+
const log = createLogger('save-lesson');
|
|
4
|
+
/** Patterns that indicate a trivial, non-educational change */
|
|
5
|
+
const TRIVIAL_PATTERNS = [
|
|
6
|
+
/^(fix|change|update)\s+(indent|format|spacing|whitespace)/i,
|
|
7
|
+
/^(add|remove)\s+(semicolon|comma|bracket|parenthes)/i,
|
|
8
|
+
/^(prettier|eslint|lint)\s+(fix|format|auto)/i,
|
|
9
|
+
/^(import|export)\s+(order|sort|organiz)/i,
|
|
10
|
+
/^(rename|typo|spell)/i,
|
|
11
|
+
];
|
|
12
|
+
/**
|
|
13
|
+
* Validate that this lesson has real educational value.
|
|
14
|
+
* Returns null if valid, or an error message if rejected.
|
|
15
|
+
*/
|
|
16
|
+
function validateLessonQuality(input) {
|
|
17
|
+
// 1. Check for trivial patterns in the title
|
|
18
|
+
for (const pattern of TRIVIAL_PATTERNS) {
|
|
19
|
+
if (pattern.test(input.title)) {
|
|
20
|
+
return `Rejected: "${input.title}" appears to be a trivial formatting/style change, not a genuine learning moment. Only save lessons for real mistakes with root cause analysis.`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// 2. Problem and root_cause should not be near-identical
|
|
24
|
+
const problemNorm = input.problem.toLowerCase().trim();
|
|
25
|
+
const rootCauseNorm = input.root_cause.toLowerCase().trim();
|
|
26
|
+
if (problemNorm === rootCauseNorm) {
|
|
27
|
+
return 'Rejected: "problem" and "root_cause" are identical. The root cause should explain WHY the problem happened, not restate the problem.';
|
|
28
|
+
}
|
|
29
|
+
// 3. If bad_code provided, good_code should also be provided (and vice versa)
|
|
30
|
+
if (input.bad_code && !input.good_code) {
|
|
31
|
+
return 'Rejected: "bad_code" was provided but "good_code" is missing. Both must be provided for a meaningful comparison.';
|
|
32
|
+
}
|
|
33
|
+
if (input.good_code && !input.bad_code) {
|
|
34
|
+
return 'Rejected: "good_code" was provided but "bad_code" is missing. Both must be provided for a meaningful comparison.';
|
|
35
|
+
}
|
|
36
|
+
// 4. Lesson should not just restate the problem
|
|
37
|
+
const lessonNorm = input.lesson.toLowerCase().trim();
|
|
38
|
+
if (lessonNorm === problemNorm) {
|
|
39
|
+
return 'Rejected: The "lesson" is just a restatement of the "problem". The lesson should be an actionable takeaway.';
|
|
40
|
+
}
|
|
41
|
+
return null; // Valid
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check for duplicate lessons (similar title + root_cause).
|
|
45
|
+
*/
|
|
46
|
+
function checkDuplicate(input) {
|
|
47
|
+
const db = getDatabase();
|
|
48
|
+
// Search FTS for similar content
|
|
49
|
+
const stmt = db.prepare(`
|
|
50
|
+
SELECT l.id, l.title
|
|
51
|
+
FROM lessons_fts fts
|
|
52
|
+
JOIN lessons l ON l.id = fts.rowid
|
|
53
|
+
WHERE lessons_fts MATCH ?
|
|
54
|
+
LIMIT 3
|
|
55
|
+
`);
|
|
56
|
+
// Build a search query from key terms in title and root_cause
|
|
57
|
+
const searchTerms = `${input.title} ${input.root_cause}`
|
|
58
|
+
.replace(/[^\w\s]/g, ' ')
|
|
59
|
+
.split(/\s+/)
|
|
60
|
+
.filter(w => w.length > 3)
|
|
61
|
+
.slice(0, 5)
|
|
62
|
+
.join(' OR ');
|
|
63
|
+
if (!searchTerms)
|
|
64
|
+
return { isDuplicate: false };
|
|
65
|
+
try {
|
|
66
|
+
const results = stmt.all(searchTerms);
|
|
67
|
+
// Simple similarity check: if any existing title is very close
|
|
68
|
+
for (const result of results) {
|
|
69
|
+
const similarity = calculateSimilarity(input.title.toLowerCase(), result.title.toLowerCase());
|
|
70
|
+
if (similarity > 0.8) {
|
|
71
|
+
return { isDuplicate: true, existingId: result.id, existingTitle: result.title };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// FTS might fail on empty table ā that's fine
|
|
77
|
+
log.debug('FTS search failed (possibly empty table), skipping duplicate check');
|
|
78
|
+
}
|
|
79
|
+
return { isDuplicate: false };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Simple Jaccard similarity between two strings (word-level).
|
|
83
|
+
*/
|
|
84
|
+
function calculateSimilarity(a, b) {
|
|
85
|
+
const wordsA = new Set(a.split(/\s+/));
|
|
86
|
+
const wordsB = new Set(b.split(/\s+/));
|
|
87
|
+
const intersection = new Set([...wordsA].filter(w => wordsB.has(w)));
|
|
88
|
+
const union = new Set([...wordsA, ...wordsB]);
|
|
89
|
+
return union.size === 0 ? 0 : intersection.size / union.size;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Save a new programming lesson to the database.
|
|
93
|
+
* Called by the MCP save_lesson tool handler.
|
|
94
|
+
*/
|
|
95
|
+
export function saveLesson(input) {
|
|
96
|
+
// Step 1: Validate lesson quality
|
|
97
|
+
const qualityError = validateLessonQuality(input);
|
|
98
|
+
if (qualityError) {
|
|
99
|
+
log.warn(`Lesson rejected: ${qualityError}`);
|
|
100
|
+
return { success: false, message: qualityError };
|
|
101
|
+
}
|
|
102
|
+
// Step 2: Check for duplicates
|
|
103
|
+
const dupCheck = checkDuplicate(input);
|
|
104
|
+
if (dupCheck.isDuplicate) {
|
|
105
|
+
const msg = `Duplicate detected: A similar lesson already exists (ID: ${dupCheck.existingId}, Title: "${dupCheck.existingTitle}"). No new lesson saved.`;
|
|
106
|
+
log.warn(msg);
|
|
107
|
+
return { success: false, message: msg };
|
|
108
|
+
}
|
|
109
|
+
// Step 3: Insert into database
|
|
110
|
+
const db = getDatabase();
|
|
111
|
+
// Calculate first review: tomorrow
|
|
112
|
+
const tomorrow = new Date();
|
|
113
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
114
|
+
const nextReviewAt = tomorrow.toISOString();
|
|
115
|
+
const stmt = db.prepare(`
|
|
116
|
+
INSERT INTO lessons (
|
|
117
|
+
title, problem, error_message, root_cause,
|
|
118
|
+
bad_code, good_code, lesson, tags, language,
|
|
119
|
+
file_path, git_diff, review_questions,
|
|
120
|
+
next_review_at, status
|
|
121
|
+
) VALUES (
|
|
122
|
+
?, ?, ?, ?,
|
|
123
|
+
?, ?, ?, ?, ?,
|
|
124
|
+
?, ?, ?,
|
|
125
|
+
?, 'new'
|
|
126
|
+
)
|
|
127
|
+
`);
|
|
128
|
+
const result = stmt.run(input.title, input.problem, input.error_message || null, input.root_cause, input.bad_code || null, input.good_code || null, input.lesson, JSON.stringify(input.tags), input.language || null, input.file_path || null, input.git_diff || null, JSON.stringify(input.review_questions), nextReviewAt);
|
|
129
|
+
const lessonId = Number(result.lastInsertRowid);
|
|
130
|
+
log.info(`Lesson saved successfully: ID=${lessonId}, Title="${input.title}"`);
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
message: `ā
Lesson saved successfully!\n\nš **${input.title}**\nš ID: ${lessonId}\nš·ļø Tags: ${input.tags.join(', ')}\nš
First review scheduled: tomorrow\n\nThe developer will be reminded to review this lesson using spaced repetition.`,
|
|
134
|
+
lessonId,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=save-lesson.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"save-lesson.js","sourceRoot":"","sources":["../../src/tools/save-lesson.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Zod schema for the save_lesson tool input.
|
|
4
|
+
* Enforces structured, high-quality lesson data from the AI assistant.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SaveLessonSchema: z.ZodObject<{
|
|
7
|
+
title: z.ZodString;
|
|
8
|
+
problem: z.ZodString;
|
|
9
|
+
error_message: z.ZodOptional<z.ZodString>;
|
|
10
|
+
root_cause: z.ZodString;
|
|
11
|
+
bad_code: z.ZodOptional<z.ZodString>;
|
|
12
|
+
good_code: z.ZodOptional<z.ZodString>;
|
|
13
|
+
lesson: z.ZodString;
|
|
14
|
+
tags: z.ZodArray<z.ZodString, "many">;
|
|
15
|
+
language: z.ZodOptional<z.ZodString>;
|
|
16
|
+
file_path: z.ZodOptional<z.ZodString>;
|
|
17
|
+
git_diff: z.ZodOptional<z.ZodString>;
|
|
18
|
+
review_questions: z.ZodArray<z.ZodObject<{
|
|
19
|
+
q: z.ZodString;
|
|
20
|
+
a: z.ZodString;
|
|
21
|
+
}, "strip", z.ZodTypeAny, {
|
|
22
|
+
q: string;
|
|
23
|
+
a: string;
|
|
24
|
+
}, {
|
|
25
|
+
q: string;
|
|
26
|
+
a: string;
|
|
27
|
+
}>, "many">;
|
|
28
|
+
}, "strip", z.ZodTypeAny, {
|
|
29
|
+
title: string;
|
|
30
|
+
problem: string;
|
|
31
|
+
root_cause: string;
|
|
32
|
+
lesson: string;
|
|
33
|
+
tags: string[];
|
|
34
|
+
review_questions: {
|
|
35
|
+
q: string;
|
|
36
|
+
a: string;
|
|
37
|
+
}[];
|
|
38
|
+
error_message?: string | undefined;
|
|
39
|
+
bad_code?: string | undefined;
|
|
40
|
+
good_code?: string | undefined;
|
|
41
|
+
language?: string | undefined;
|
|
42
|
+
file_path?: string | undefined;
|
|
43
|
+
git_diff?: string | undefined;
|
|
44
|
+
}, {
|
|
45
|
+
title: string;
|
|
46
|
+
problem: string;
|
|
47
|
+
root_cause: string;
|
|
48
|
+
lesson: string;
|
|
49
|
+
tags: string[];
|
|
50
|
+
review_questions: {
|
|
51
|
+
q: string;
|
|
52
|
+
a: string;
|
|
53
|
+
}[];
|
|
54
|
+
error_message?: string | undefined;
|
|
55
|
+
bad_code?: string | undefined;
|
|
56
|
+
good_code?: string | undefined;
|
|
57
|
+
language?: string | undefined;
|
|
58
|
+
file_path?: string | undefined;
|
|
59
|
+
git_diff?: string | undefined;
|
|
60
|
+
}>;
|
|
61
|
+
export type SaveLessonInput = z.infer<typeof SaveLessonSchema>;
|
|
62
|
+
/**
|
|
63
|
+
* Zod schema for the memory (search) tool input.
|
|
64
|
+
*/
|
|
65
|
+
export declare const MemorySchema: z.ZodObject<{
|
|
66
|
+
query: z.ZodString;
|
|
67
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
68
|
+
language: z.ZodOptional<z.ZodString>;
|
|
69
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
70
|
+
}, "strip", z.ZodTypeAny, {
|
|
71
|
+
query: string;
|
|
72
|
+
limit: number;
|
|
73
|
+
tags?: string[] | undefined;
|
|
74
|
+
language?: string | undefined;
|
|
75
|
+
}, {
|
|
76
|
+
query: string;
|
|
77
|
+
tags?: string[] | undefined;
|
|
78
|
+
language?: string | undefined;
|
|
79
|
+
limit?: number | undefined;
|
|
80
|
+
}>;
|
|
81
|
+
export type MemoryInput = z.infer<typeof MemorySchema>;
|
|
82
|
+
//# sourceMappingURL=validators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../src/tools/validators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoE3B,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE/D;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;EAuBvB,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC"}
|