@bis-code/study-dash 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.
Files changed (89) hide show
  1. package/.claude-plugin/marketplace.json +18 -0
  2. package/.claude-plugin/plugin.json +19 -0
  3. package/.mcp.json +8 -0
  4. package/LICENSE +21 -0
  5. package/commands/dashboard.md +8 -0
  6. package/commands/import.md +12 -0
  7. package/commands/learn.md +13 -0
  8. package/hooks/hooks.json +27 -0
  9. package/package.json +36 -0
  10. package/rules/tutor-mode.md +38 -0
  11. package/server/dist/bundle.mjs +1240 -0
  12. package/server/dist/dashboard/api.d.ts +16 -0
  13. package/server/dist/dashboard/api.js +150 -0
  14. package/server/dist/dashboard/api.js.map +1 -0
  15. package/server/dist/dashboard/server.d.ts +21 -0
  16. package/server/dist/dashboard/server.js +171 -0
  17. package/server/dist/dashboard/server.js.map +1 -0
  18. package/server/dist/index.d.ts +2 -0
  19. package/server/dist/index.js +40 -0
  20. package/server/dist/index.js.map +1 -0
  21. package/server/dist/services/curriculum.d.ts +25 -0
  22. package/server/dist/services/curriculum.js +110 -0
  23. package/server/dist/services/curriculum.js.map +1 -0
  24. package/server/dist/services/exercises.d.ts +35 -0
  25. package/server/dist/services/exercises.js +215 -0
  26. package/server/dist/services/exercises.js.map +1 -0
  27. package/server/dist/services/qa.d.ts +15 -0
  28. package/server/dist/services/qa.js +30 -0
  29. package/server/dist/services/qa.js.map +1 -0
  30. package/server/dist/services/viz.d.ts +8 -0
  31. package/server/dist/services/viz.js +21 -0
  32. package/server/dist/services/viz.js.map +1 -0
  33. package/server/dist/storage/db.d.ts +11 -0
  34. package/server/dist/storage/db.js +51 -0
  35. package/server/dist/storage/db.js.map +1 -0
  36. package/server/dist/storage/files.d.ts +10 -0
  37. package/server/dist/storage/files.js +34 -0
  38. package/server/dist/storage/files.js.map +1 -0
  39. package/server/dist/storage/schema.d.ts +3 -0
  40. package/server/dist/storage/schema.js +126 -0
  41. package/server/dist/storage/schema.js.map +1 -0
  42. package/server/dist/tools/curriculum.d.ts +4 -0
  43. package/server/dist/tools/curriculum.js +137 -0
  44. package/server/dist/tools/curriculum.js.map +1 -0
  45. package/server/dist/tools/exercises.d.ts +4 -0
  46. package/server/dist/tools/exercises.js +76 -0
  47. package/server/dist/tools/exercises.js.map +1 -0
  48. package/server/dist/tools/qa.d.ts +4 -0
  49. package/server/dist/tools/qa.js +56 -0
  50. package/server/dist/tools/qa.js.map +1 -0
  51. package/server/dist/tools/viz.d.ts +4 -0
  52. package/server/dist/tools/viz.js +54 -0
  53. package/server/dist/tools/viz.js.map +1 -0
  54. package/server/dist/types.d.ts +103 -0
  55. package/server/dist/types.js +2 -0
  56. package/server/dist/types.js.map +1 -0
  57. package/server/node_modules/better-sqlite3/LICENSE +21 -0
  58. package/server/node_modules/better-sqlite3/README.md +99 -0
  59. package/server/node_modules/better-sqlite3/binding.gyp +38 -0
  60. package/server/node_modules/better-sqlite3/build/Release/better_sqlite3.node +0 -0
  61. package/server/node_modules/better-sqlite3/deps/common.gypi +68 -0
  62. package/server/node_modules/better-sqlite3/deps/copy.js +31 -0
  63. package/server/node_modules/better-sqlite3/deps/defines.gypi +41 -0
  64. package/server/node_modules/better-sqlite3/deps/download.sh +122 -0
  65. package/server/node_modules/better-sqlite3/deps/patches/1208.patch +15 -0
  66. package/server/node_modules/better-sqlite3/deps/sqlite3/sqlite3.c +261480 -0
  67. package/server/node_modules/better-sqlite3/deps/sqlite3/sqlite3.h +13715 -0
  68. package/server/node_modules/better-sqlite3/deps/sqlite3/sqlite3ext.h +719 -0
  69. package/server/node_modules/better-sqlite3/deps/sqlite3.gyp +80 -0
  70. package/server/node_modules/better-sqlite3/deps/test_extension.c +21 -0
  71. package/server/node_modules/better-sqlite3/lib/database.js +90 -0
  72. package/server/node_modules/better-sqlite3/lib/index.js +3 -0
  73. package/server/node_modules/better-sqlite3/lib/methods/aggregate.js +43 -0
  74. package/server/node_modules/better-sqlite3/lib/methods/backup.js +67 -0
  75. package/server/node_modules/better-sqlite3/lib/methods/function.js +31 -0
  76. package/server/node_modules/better-sqlite3/lib/methods/inspect.js +7 -0
  77. package/server/node_modules/better-sqlite3/lib/methods/pragma.js +12 -0
  78. package/server/node_modules/better-sqlite3/lib/methods/serialize.js +16 -0
  79. package/server/node_modules/better-sqlite3/lib/methods/table.js +189 -0
  80. package/server/node_modules/better-sqlite3/lib/methods/transaction.js +78 -0
  81. package/server/node_modules/better-sqlite3/lib/methods/wrappers.js +54 -0
  82. package/server/node_modules/better-sqlite3/lib/sqlite-error.js +20 -0
  83. package/server/node_modules/better-sqlite3/lib/util.js +12 -0
  84. package/server/node_modules/better-sqlite3/package.json +54 -0
  85. package/server/node_modules/better-sqlite3/src/better_sqlite3.cpp +2186 -0
  86. package/server/node_modules/better-sqlite3/src/better_sqlite3.hpp +1036 -0
  87. package/server/package.json +31 -0
  88. package/skills/import/SKILL.md +19 -0
  89. package/skills/learn/SKILL.md +17 -0
@@ -0,0 +1,215 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const execFileAsync = promisify(execFile);
4
+ function slugify(name) {
5
+ return name
6
+ .toLowerCase()
7
+ .replace(/[^a-z0-9]+/g, '-')
8
+ .replace(/^-+|-+$/g, '');
9
+ }
10
+ function extensionForLanguage(language) {
11
+ switch (language) {
12
+ case 'go':
13
+ return '.go';
14
+ case 'python':
15
+ return '.py';
16
+ case 'rust':
17
+ return '.rs';
18
+ case 'javascript':
19
+ case 'typescript':
20
+ return '.ts';
21
+ default:
22
+ return '.txt';
23
+ }
24
+ }
25
+ export class ExerciseService {
26
+ db;
27
+ fileStore;
28
+ constructor(db, fileStore) {
29
+ this.db = db;
30
+ this.fileStore = fileStore;
31
+ }
32
+ createExercise(topicId, data) {
33
+ const { title, type, description, difficulty = 'medium', est_minutes = 0, source = 'ai', starter_code = '', test_content = '', quiz_json = '{}', } = data;
34
+ const result = this.db.raw
35
+ .prepare(`INSERT INTO exercises
36
+ (topic_id, title, type, description, difficulty, est_minutes, source, starter_code, test_content, quiz_json)
37
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
38
+ RETURNING id`)
39
+ .get(topicId, title, type, description, difficulty, est_minutes, source, starter_code, test_content, quiz_json);
40
+ const exerciseId = result.id;
41
+ // Write files for coding/project exercises with starter code
42
+ if ((type === 'coding' || type === 'project') && (starter_code || test_content)) {
43
+ const subject = this.getSubjectForTopic(topicId);
44
+ if (subject) {
45
+ const exerciseSlug = slugify(title);
46
+ const ext = extensionForLanguage(subject.language);
47
+ const files = {};
48
+ if (starter_code) {
49
+ files[`main${ext}`] = starter_code;
50
+ }
51
+ if (test_content) {
52
+ files[`main_test${ext}`] = test_content;
53
+ }
54
+ files['README.md'] = `# ${title}\n\n${description}`;
55
+ const filePath = this.fileStore.writeExerciseFiles(subject.slug, exerciseSlug, files);
56
+ this.db.raw
57
+ .prepare('UPDATE exercises SET file_path = ? WHERE id = ?')
58
+ .run(filePath, exerciseId);
59
+ }
60
+ }
61
+ return this.db.raw
62
+ .prepare('SELECT * FROM exercises WHERE id = ?')
63
+ .get(exerciseId);
64
+ }
65
+ async runTests(exerciseId) {
66
+ const exercise = this.db.raw
67
+ .prepare('SELECT * FROM exercises WHERE id = ?')
68
+ .get(exerciseId);
69
+ if (!exercise)
70
+ throw new Error(`Exercise ${exerciseId} not found`);
71
+ if (!exercise.file_path)
72
+ throw new Error(`Exercise ${exerciseId} has no file_path`);
73
+ const subject = this.getSubjectForTopic(exercise.topic_id);
74
+ if (!subject)
75
+ throw new Error(`No subject found for exercise ${exerciseId}`);
76
+ const commandMap = {
77
+ go: { command: 'go', args: ['test', '-json', '-count=1', './...'] },
78
+ python: { command: 'python3', args: ['-m', 'pytest', '--tb=short', '-q', '.'] },
79
+ rust: { command: 'cargo', args: ['test'] },
80
+ javascript: { command: 'npx', args: ['vitest', 'run'] },
81
+ typescript: { command: 'npx', args: ['vitest', 'run'] },
82
+ };
83
+ const config = commandMap[subject.language];
84
+ if (!config)
85
+ throw new Error(`Unsupported language: ${subject.language}`);
86
+ let stdout = '';
87
+ let stderr = '';
88
+ let exitCode = 0;
89
+ try {
90
+ const result = await execFileAsync(config.command, config.args, {
91
+ cwd: exercise.file_path,
92
+ timeout: 60_000,
93
+ });
94
+ stdout = result.stdout;
95
+ stderr = result.stderr;
96
+ }
97
+ catch (err) {
98
+ const execErr = err;
99
+ stdout = execErr.stdout ?? '';
100
+ stderr = execErr.stderr ?? '';
101
+ exitCode = execErr.code ?? 1;
102
+ }
103
+ // Parse results
104
+ const results = [];
105
+ if (subject.language === 'go') {
106
+ // Parse Go JSON test output
107
+ for (const line of stdout.split('\n')) {
108
+ if (!line.trim())
109
+ continue;
110
+ try {
111
+ const event = JSON.parse(line);
112
+ if (event.Action === 'pass' && event.Test) {
113
+ results.push({ test_name: event.Test, passed: true, output: '' });
114
+ }
115
+ else if (event.Action === 'fail' && event.Test) {
116
+ results.push({ test_name: event.Test, passed: false, output: event.Output ?? '' });
117
+ }
118
+ }
119
+ catch {
120
+ // Skip non-JSON lines
121
+ }
122
+ }
123
+ }
124
+ // Fallback: if no per-test results parsed, use overall result
125
+ if (results.length === 0) {
126
+ results.push({
127
+ test_name: 'all',
128
+ passed: exitCode === 0,
129
+ output: stdout + stderr,
130
+ });
131
+ }
132
+ // Clear old results
133
+ this.db.raw
134
+ .prepare('DELETE FROM exercise_results WHERE exercise_id = ?')
135
+ .run(exerciseId);
136
+ // Insert new results
137
+ const insertResult = this.db.raw.prepare('INSERT INTO exercise_results (exercise_id, test_name, passed, output) VALUES (?, ?, ?, ?)');
138
+ for (const r of results) {
139
+ insertResult.run(exerciseId, r.test_name, r.passed ? 1 : 0, r.output);
140
+ }
141
+ // Update exercise status
142
+ const allPassed = results.every((r) => r.passed);
143
+ this.db.raw
144
+ .prepare('UPDATE exercises SET status = ? WHERE id = ?')
145
+ .run(allPassed ? 'passed' : 'failed', exerciseId);
146
+ return this.db.raw
147
+ .prepare('SELECT * FROM exercise_results WHERE exercise_id = ?')
148
+ .all(exerciseId);
149
+ }
150
+ submitQuiz(exerciseId, answers) {
151
+ const exercise = this.db.raw
152
+ .prepare('SELECT * FROM exercises WHERE id = ?')
153
+ .get(exerciseId);
154
+ if (!exercise)
155
+ throw new Error(`Exercise ${exerciseId} not found`);
156
+ const payload = JSON.parse(exercise.quiz_json);
157
+ const questions = payload.questions;
158
+ let correct = 0;
159
+ const results = [];
160
+ for (let i = 0; i < questions.length; i++) {
161
+ const q = questions[i];
162
+ const answer = answers[i];
163
+ let isCorrect = false;
164
+ switch (q.type) {
165
+ case 'multiple_choice':
166
+ isCorrect = answer === q.correct;
167
+ break;
168
+ case 'true_false':
169
+ isCorrect = answer === q.correct;
170
+ break;
171
+ case 'fill_in':
172
+ isCorrect =
173
+ String(answer).toLowerCase().trim() === String(q.correct).toLowerCase().trim();
174
+ break;
175
+ }
176
+ if (isCorrect)
177
+ correct++;
178
+ results.push({
179
+ test_name: `Q${i + 1}: ${q.text}`,
180
+ passed: isCorrect,
181
+ output: isCorrect ? 'Correct' : `Wrong. Expected: ${q.correct}, Got: ${answer}`,
182
+ });
183
+ }
184
+ const score = questions.length > 0 ? correct / questions.length : 0;
185
+ const passed = score >= 0.7;
186
+ // Clear old results
187
+ this.db.raw
188
+ .prepare('DELETE FROM exercise_results WHERE exercise_id = ?')
189
+ .run(exerciseId);
190
+ // Insert per-question results
191
+ const insertResult = this.db.raw.prepare('INSERT INTO exercise_results (exercise_id, test_name, passed, output) VALUES (?, ?, ?, ?)');
192
+ for (const r of results) {
193
+ insertResult.run(exerciseId, r.test_name, r.passed ? 1 : 0, r.output);
194
+ }
195
+ // Update exercise status
196
+ this.db.raw
197
+ .prepare('UPDATE exercises SET status = ? WHERE id = ?')
198
+ .run(passed ? 'passed' : 'failed', exerciseId);
199
+ return { score, total: questions.length, passed, results };
200
+ }
201
+ listForTopic(topicId) {
202
+ return this.db.raw
203
+ .prepare('SELECT * FROM exercises WHERE topic_id = ? ORDER BY created_at ASC, id ASC')
204
+ .all(topicId);
205
+ }
206
+ getSubjectForTopic(topicId) {
207
+ return this.db.raw
208
+ .prepare(`SELECT s.* FROM subjects s
209
+ JOIN phases p ON p.subject_id = s.id
210
+ JOIN topics t ON t.phase_id = p.id
211
+ WHERE t.id = ?`)
212
+ .get(topicId);
213
+ }
214
+ }
215
+ //# sourceMappingURL=exercises.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exercises.js","sourceRoot":"","sources":["../../src/services/exercises.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { Database } from '../storage/db.js';
2
+ import type { Entry } from '../types.js';
3
+ export declare class QAService {
4
+ private db;
5
+ constructor(db: Database);
6
+ logEntry(topicId: number, kind: Entry['kind'], content: string, sessionId?: string, questionId?: number): Entry;
7
+ listEntries(topicId: number): Entry[];
8
+ search(query: string): {
9
+ id: number;
10
+ topic_id: number;
11
+ kind: string;
12
+ content: string;
13
+ created_at: string;
14
+ }[];
15
+ }
@@ -0,0 +1,30 @@
1
+ export class QAService {
2
+ db;
3
+ constructor(db) {
4
+ this.db = db;
5
+ }
6
+ logEntry(topicId, kind, content, sessionId, questionId) {
7
+ const result = this.db.raw
8
+ .prepare('INSERT INTO entries (topic_id, kind, content, session_id, question_id) VALUES (?, ?, ?, ?, ?) RETURNING id')
9
+ .get(topicId, kind, content, sessionId ?? '', questionId ?? null);
10
+ return this.db.raw
11
+ .prepare('SELECT * FROM entries WHERE id = ?')
12
+ .get(result.id);
13
+ }
14
+ listEntries(topicId) {
15
+ return this.db.raw
16
+ .prepare('SELECT * FROM entries WHERE topic_id = ? ORDER BY created_at ASC')
17
+ .all(topicId);
18
+ }
19
+ search(query) {
20
+ return this.db.raw
21
+ .prepare(`SELECT e.id, e.topic_id, e.kind, e.content, e.created_at
22
+ FROM entries e
23
+ JOIN entries_fts ON entries_fts.rowid = e.id
24
+ WHERE entries_fts MATCH ?
25
+ ORDER BY e.created_at ASC
26
+ LIMIT 50`)
27
+ .all(query);
28
+ }
29
+ }
30
+ //# sourceMappingURL=qa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qa.js","sourceRoot":"","sources":["../../src/services/qa.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,SAAS;IACA;IAApB,YAAoB,EAAY;QAAZ,OAAE,GAAF,EAAE,CAAU;IAAG,CAAC;IAEpC,QAAQ,CACN,OAAe,EACf,IAAmB,EACnB,OAAe,EACf,SAAkB,EAClB,UAAmB;QAEnB,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG;aACvB,OAAO,CAIN,4GAA4G,CAC7G;aACA,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,IAAI,EAAE,EAAE,UAAU,IAAI,IAAI,CAAC,CAAC;QAEpE,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG;aACf,OAAO,CAAkB,oCAAoC,CAAC;aAC9D,GAAG,CAAC,MAAO,CAAC,EAAE,CAAE,CAAC;IACtB,CAAC;IAED,WAAW,CAAC,OAAe;QACzB,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG;aACf,OAAO,CACN,kEAAkE,CACnE;aACA,GAAG,CAAC,OAAO,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,CAAC,KAAa;QAClB,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG;aACf,OAAO,CAIN;;;;;kBAKU,CACX;aACA,GAAG,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ import { Database } from '../storage/db.js';
2
+ import type { Visualization, VizStep } from '../types.js';
3
+ export declare class VizService {
4
+ private db;
5
+ constructor(db: Database);
6
+ create(topicId: number, title: string, steps: VizStep[]): Visualization;
7
+ listForTopic(topicId: number): Visualization[];
8
+ }
@@ -0,0 +1,21 @@
1
+ export class VizService {
2
+ db;
3
+ constructor(db) {
4
+ this.db = db;
5
+ }
6
+ create(topicId, title, steps) {
7
+ const stepsJson = JSON.stringify(steps);
8
+ const result = this.db.raw
9
+ .prepare('INSERT INTO visualizations (topic_id, title, steps_json) VALUES (?, ?, ?) RETURNING id')
10
+ .get(topicId, title, stepsJson);
11
+ return this.db.raw
12
+ .prepare('SELECT * FROM visualizations WHERE id = ?')
13
+ .get(result.id);
14
+ }
15
+ listForTopic(topicId) {
16
+ return this.db.raw
17
+ .prepare('SELECT * FROM visualizations WHERE topic_id = ? ORDER BY created_at DESC, id DESC')
18
+ .all(topicId);
19
+ }
20
+ }
21
+ //# sourceMappingURL=viz.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viz.js","sourceRoot":"","sources":["../../src/services/viz.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,UAAU;IACD;IAApB,YAAoB,EAAY;QAAZ,OAAE,GAAF,EAAE,CAAU;IAAG,CAAC;IAEpC,MAAM,CAAC,OAAe,EAAE,KAAa,EAAE,KAAgB;QACrD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAExC,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG;aACvB,OAAO,CACN,wFAAwF,CACzF;aACA,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;QAElC,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG;aACf,OAAO,CAA0B,2CAA2C,CAAC;aAC7E,GAAG,CAAC,MAAO,CAAC,EAAE,CAAE,CAAC;IACtB,CAAC;IAED,YAAY,CAAC,OAAe;QAC1B,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG;aACf,OAAO,CACN,mFAAmF,CACpF;aACA,GAAG,CAAC,OAAO,CAAC,CAAC;IAClB,CAAC;CACF"}
@@ -0,0 +1,11 @@
1
+ import BetterSqlite3 from 'better-sqlite3';
2
+ export declare class Database {
3
+ private db;
4
+ constructor(dbPath: string);
5
+ getSetting(key: string): string | undefined;
6
+ setSetting(key: string, value: string): void;
7
+ listTables(): string[];
8
+ /** Expose the raw better-sqlite3 handle for advanced operations. */
9
+ get raw(): BetterSqlite3.Database;
10
+ close(): void;
11
+ }
@@ -0,0 +1,51 @@
1
+ import BetterSqlite3 from 'better-sqlite3';
2
+ import { schema, migrations } from './schema.js';
3
+ export class Database {
4
+ db;
5
+ constructor(dbPath) {
6
+ this.db = new BetterSqlite3(dbPath);
7
+ // Performance + integrity PRAGMAs
8
+ this.db.pragma('journal_mode=WAL');
9
+ this.db.pragma('foreign_keys=ON');
10
+ // Apply schema (all CREATE IF NOT EXISTS — safe to re-run)
11
+ this.db.exec(schema);
12
+ // Seed defaults on first initialisation
13
+ const currentVersion = this.getSetting('schema_version');
14
+ if (!currentVersion) {
15
+ this.setSetting('schema_version', '1');
16
+ this.setSetting('auto_viz', 'true');
17
+ this.setSetting('dashboard_port', '19282');
18
+ }
19
+ // Run any pending migrations beyond the baseline
20
+ const versionNum = parseInt(this.getSetting('schema_version') ?? '1', 10);
21
+ for (let i = versionNum - 1; i < migrations.length; i++) {
22
+ this.db.exec(migrations[i]);
23
+ this.setSetting('schema_version', String(i + 2));
24
+ }
25
+ }
26
+ getSetting(key) {
27
+ const row = this.db
28
+ .prepare('SELECT value FROM settings WHERE key = ?')
29
+ .get(key);
30
+ return row?.value;
31
+ }
32
+ setSetting(key, value) {
33
+ this.db
34
+ .prepare('INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value')
35
+ .run(key, value);
36
+ }
37
+ listTables() {
38
+ const allRows = this.db
39
+ .prepare("SELECT name FROM sqlite_master WHERE type IN ('table')")
40
+ .all();
41
+ return allRows.map((r) => r.name);
42
+ }
43
+ /** Expose the raw better-sqlite3 handle for advanced operations. */
44
+ get raw() {
45
+ return this.db;
46
+ }
47
+ close() {
48
+ this.db.close();
49
+ }
50
+ }
51
+ //# sourceMappingURL=db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/storage/db.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,OAAO,QAAQ;IACX,EAAE,CAAyB;IAEnC,YAAY,MAAc;QACxB,IAAI,CAAC,EAAE,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,CAAC;QAEpC,kCAAkC;QAClC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC;QACnC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;QAElC,2DAA2D;QAC3D,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAErB,wCAAwC;QACxC,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;QACzD,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,IAAI,CAAC,UAAU,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;YACvC,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;YACpC,IAAI,CAAC,UAAU,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QAC7C,CAAC;QAED,iDAAiD;QACjD,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QAC1E,KAAK,IAAI,CAAC,GAAG,UAAU,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxD,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5B,IAAI,CAAC,UAAU,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IAED,UAAU,CAAC,GAAW;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE;aAChB,OAAO,CAA8B,0CAA0C,CAAC;aAChF,GAAG,CAAC,GAAG,CAAC,CAAC;QACZ,OAAO,GAAG,EAAE,KAAK,CAAC;IACpB,CAAC;IAED,UAAU,CAAC,GAAW,EAAE,KAAa;QACnC,IAAI,CAAC,EAAE;aACJ,OAAO,CAAC,uGAAuG,CAAC;aAChH,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrB,CAAC;IAED,UAAU;QACR,MAAM,OAAO,GAAG,IAAI,CAAC,EAAE;aACpB,OAAO,CAAuB,wDAAwD,CAAC;aACvF,GAAG,EAAE,CAAC;QACT,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,oEAAoE;IACpE,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,EAAE,CAAC;IACjB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;CACF"}
@@ -0,0 +1,10 @@
1
+ export declare class FileStore {
2
+ private baseDir;
3
+ constructor(baseDir?: string);
4
+ get exercisesDir(): string;
5
+ get dataDir(): string;
6
+ get dbPath(): string;
7
+ writeExerciseFiles(subjectSlug: string, exerciseSlug: string, files: Record<string, string>): string;
8
+ exerciseExists(subjectSlug: string, exerciseSlug: string): boolean;
9
+ readFile(path: string): string;
10
+ }
@@ -0,0 +1,34 @@
1
+ import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ export class FileStore {
5
+ baseDir;
6
+ constructor(baseDir) {
7
+ this.baseDir = baseDir ?? join(homedir(), '.claude', 'learn');
8
+ mkdirSync(join(this.baseDir, 'exercises'), { recursive: true });
9
+ }
10
+ get exercisesDir() {
11
+ return join(this.baseDir, 'exercises');
12
+ }
13
+ get dataDir() {
14
+ return this.baseDir;
15
+ }
16
+ get dbPath() {
17
+ return join(this.baseDir, 'data.db');
18
+ }
19
+ writeExerciseFiles(subjectSlug, exerciseSlug, files) {
20
+ const dir = join(this.exercisesDir, subjectSlug, exerciseSlug);
21
+ mkdirSync(dir, { recursive: true });
22
+ for (const [name, content] of Object.entries(files)) {
23
+ writeFileSync(join(dir, name), content, 'utf-8');
24
+ }
25
+ return dir;
26
+ }
27
+ exerciseExists(subjectSlug, exerciseSlug) {
28
+ return existsSync(join(this.exercisesDir, subjectSlug, exerciseSlug));
29
+ }
30
+ readFile(path) {
31
+ return readFileSync(path, 'utf-8');
32
+ }
33
+ }
34
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/storage/files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,MAAM,OAAO,SAAS;IACZ,OAAO,CAAS;IAExB,YAAY,OAAgB;QAC1B,IAAI,CAAC,OAAO,GAAG,OAAO,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QAC9D,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClE,CAAC;IAED,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IACzC,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACvC,CAAC;IAED,kBAAkB,CAAC,WAAmB,EAAE,YAAoB,EAAE,KAA6B;QACzF,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC;QAC/D,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACpD,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QACnD,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,cAAc,CAAC,WAAmB,EAAE,YAAoB;QACtD,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,QAAQ,CAAC,IAAY;QACnB,OAAO,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACrC,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ export declare const schema = "\nPRAGMA foreign_keys=ON;\n\nCREATE TABLE IF NOT EXISTS subjects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n language TEXT NOT NULL DEFAULT '',\n source TEXT NOT NULL DEFAULT 'manual'\n CHECK (source IN ('manual','roadmap','pdf')),\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS phases (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n subject_id INTEGER NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n description TEXT NOT NULL DEFAULT '',\n sort_order INTEGER NOT NULL DEFAULT 0\n);\n\nCREATE INDEX IF NOT EXISTS idx_phases_subject ON phases(subject_id);\n\nCREATE TABLE IF NOT EXISTS topics (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n phase_id INTEGER NOT NULL REFERENCES phases(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n description TEXT NOT NULL DEFAULT '',\n sort_order INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'todo'\n CHECK (status IN ('todo','in_progress','done')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_topics_phase ON topics(phase_id);\nCREATE INDEX IF NOT EXISTS idx_topics_status ON topics(status);\n\nCREATE TABLE IF NOT EXISTS entries (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n kind TEXT NOT NULL CHECK (kind IN ('question','answer','note')),\n content TEXT NOT NULL DEFAULT '',\n session_id TEXT NOT NULL DEFAULT '',\n question_id INTEGER REFERENCES entries(id) ON DELETE SET NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_entries_topic ON entries(topic_id);\nCREATE INDEX IF NOT EXISTS idx_entries_session ON entries(session_id);\n\nCREATE TABLE IF NOT EXISTS visualizations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n title TEXT NOT NULL DEFAULT '',\n steps_json TEXT NOT NULL DEFAULT '[]',\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_viz_topic ON visualizations(topic_id);\n\nCREATE TABLE IF NOT EXISTS exercises (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n title TEXT NOT NULL DEFAULT '',\n type TEXT NOT NULL DEFAULT 'coding'\n CHECK (type IN ('coding','quiz','project','assignment')),\n description TEXT NOT NULL DEFAULT '',\n difficulty TEXT NOT NULL DEFAULT 'medium'\n CHECK (difficulty IN ('easy','medium','hard')),\n est_minutes INTEGER NOT NULL DEFAULT 0,\n source TEXT NOT NULL DEFAULT 'ai'\n CHECK (source IN ('ai','pdf_import')),\n starter_code TEXT NOT NULL DEFAULT '',\n test_content TEXT NOT NULL DEFAULT '',\n quiz_json TEXT NOT NULL DEFAULT '{}',\n file_path TEXT NOT NULL DEFAULT '',\n status TEXT NOT NULL DEFAULT 'pending'\n CHECK (status IN ('pending','in_progress','passed','failed')),\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_exercises_topic ON exercises(topic_id);\nCREATE INDEX IF NOT EXISTS idx_exercises_status ON exercises(status);\n\nCREATE TABLE IF NOT EXISTS exercise_results (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n exercise_id INTEGER NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,\n test_name TEXT NOT NULL DEFAULT '',\n passed INTEGER NOT NULL DEFAULT 0,\n output TEXT NOT NULL DEFAULT '',\n ran_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_results_exercise ON exercise_results(exercise_id);\n\nCREATE TABLE IF NOT EXISTS settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL DEFAULT ''\n);\n\n-- FTS5 virtual table for full-text search over entries\nCREATE VIRTUAL TABLE IF NOT EXISTS entries_fts\n USING fts5(content, content='entries', content_rowid='id');\n\n-- Sync triggers: keep entries_fts up to date with entries\nCREATE TRIGGER IF NOT EXISTS entries_ai\n AFTER INSERT ON entries BEGIN\n INSERT INTO entries_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\nCREATE TRIGGER IF NOT EXISTS entries_ad\n AFTER DELETE ON entries BEGIN\n INSERT INTO entries_fts(entries_fts, rowid, content)\n VALUES ('delete', old.id, old.content);\n END;\n\nCREATE TRIGGER IF NOT EXISTS entries_au\n AFTER UPDATE ON entries BEGIN\n INSERT INTO entries_fts(entries_fts, rowid, content)\n VALUES ('delete', old.id, old.content);\n INSERT INTO entries_fts(rowid, content) VALUES (new.id, new.content);\n END;\n";
2
+ /** Future migrations appended here in order (v1 is baseline — empty). */
3
+ export declare const migrations: string[];
@@ -0,0 +1,126 @@
1
+ export const schema = `
2
+ PRAGMA foreign_keys=ON;
3
+
4
+ CREATE TABLE IF NOT EXISTS subjects (
5
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ name TEXT NOT NULL,
7
+ slug TEXT NOT NULL UNIQUE,
8
+ language TEXT NOT NULL DEFAULT '',
9
+ source TEXT NOT NULL DEFAULT 'manual'
10
+ CHECK (source IN ('manual','roadmap','pdf')),
11
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
12
+ );
13
+
14
+ CREATE TABLE IF NOT EXISTS phases (
15
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16
+ subject_id INTEGER NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,
17
+ name TEXT NOT NULL,
18
+ description TEXT NOT NULL DEFAULT '',
19
+ sort_order INTEGER NOT NULL DEFAULT 0
20
+ );
21
+
22
+ CREATE INDEX IF NOT EXISTS idx_phases_subject ON phases(subject_id);
23
+
24
+ CREATE TABLE IF NOT EXISTS topics (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ phase_id INTEGER NOT NULL REFERENCES phases(id) ON DELETE CASCADE,
27
+ name TEXT NOT NULL,
28
+ description TEXT NOT NULL DEFAULT '',
29
+ sort_order INTEGER NOT NULL DEFAULT 0,
30
+ status TEXT NOT NULL DEFAULT 'todo'
31
+ CHECK (status IN ('todo','in_progress','done')),
32
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
33
+ );
34
+
35
+ CREATE INDEX IF NOT EXISTS idx_topics_phase ON topics(phase_id);
36
+ CREATE INDEX IF NOT EXISTS idx_topics_status ON topics(status);
37
+
38
+ CREATE TABLE IF NOT EXISTS entries (
39
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
40
+ topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
41
+ kind TEXT NOT NULL CHECK (kind IN ('question','answer','note')),
42
+ content TEXT NOT NULL DEFAULT '',
43
+ session_id TEXT NOT NULL DEFAULT '',
44
+ question_id INTEGER REFERENCES entries(id) ON DELETE SET NULL,
45
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
46
+ );
47
+
48
+ CREATE INDEX IF NOT EXISTS idx_entries_topic ON entries(topic_id);
49
+ CREATE INDEX IF NOT EXISTS idx_entries_session ON entries(session_id);
50
+
51
+ CREATE TABLE IF NOT EXISTS visualizations (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
54
+ title TEXT NOT NULL DEFAULT '',
55
+ steps_json TEXT NOT NULL DEFAULT '[]',
56
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
57
+ );
58
+
59
+ CREATE INDEX IF NOT EXISTS idx_viz_topic ON visualizations(topic_id);
60
+
61
+ CREATE TABLE IF NOT EXISTS exercises (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
64
+ title TEXT NOT NULL DEFAULT '',
65
+ type TEXT NOT NULL DEFAULT 'coding'
66
+ CHECK (type IN ('coding','quiz','project','assignment')),
67
+ description TEXT NOT NULL DEFAULT '',
68
+ difficulty TEXT NOT NULL DEFAULT 'medium'
69
+ CHECK (difficulty IN ('easy','medium','hard')),
70
+ est_minutes INTEGER NOT NULL DEFAULT 0,
71
+ source TEXT NOT NULL DEFAULT 'ai'
72
+ CHECK (source IN ('ai','pdf_import')),
73
+ starter_code TEXT NOT NULL DEFAULT '',
74
+ test_content TEXT NOT NULL DEFAULT '',
75
+ quiz_json TEXT NOT NULL DEFAULT '{}',
76
+ file_path TEXT NOT NULL DEFAULT '',
77
+ status TEXT NOT NULL DEFAULT 'pending'
78
+ CHECK (status IN ('pending','in_progress','passed','failed')),
79
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
80
+ );
81
+
82
+ CREATE INDEX IF NOT EXISTS idx_exercises_topic ON exercises(topic_id);
83
+ CREATE INDEX IF NOT EXISTS idx_exercises_status ON exercises(status);
84
+
85
+ CREATE TABLE IF NOT EXISTS exercise_results (
86
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
87
+ exercise_id INTEGER NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,
88
+ test_name TEXT NOT NULL DEFAULT '',
89
+ passed INTEGER NOT NULL DEFAULT 0,
90
+ output TEXT NOT NULL DEFAULT '',
91
+ ran_at TEXT NOT NULL DEFAULT (datetime('now'))
92
+ );
93
+
94
+ CREATE INDEX IF NOT EXISTS idx_results_exercise ON exercise_results(exercise_id);
95
+
96
+ CREATE TABLE IF NOT EXISTS settings (
97
+ key TEXT PRIMARY KEY,
98
+ value TEXT NOT NULL DEFAULT ''
99
+ );
100
+
101
+ -- FTS5 virtual table for full-text search over entries
102
+ CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts
103
+ USING fts5(content, content='entries', content_rowid='id');
104
+
105
+ -- Sync triggers: keep entries_fts up to date with entries
106
+ CREATE TRIGGER IF NOT EXISTS entries_ai
107
+ AFTER INSERT ON entries BEGIN
108
+ INSERT INTO entries_fts(rowid, content) VALUES (new.id, new.content);
109
+ END;
110
+
111
+ CREATE TRIGGER IF NOT EXISTS entries_ad
112
+ AFTER DELETE ON entries BEGIN
113
+ INSERT INTO entries_fts(entries_fts, rowid, content)
114
+ VALUES ('delete', old.id, old.content);
115
+ END;
116
+
117
+ CREATE TRIGGER IF NOT EXISTS entries_au
118
+ AFTER UPDATE ON entries BEGIN
119
+ INSERT INTO entries_fts(entries_fts, rowid, content)
120
+ VALUES ('delete', old.id, old.content);
121
+ INSERT INTO entries_fts(rowid, content) VALUES (new.id, new.content);
122
+ END;
123
+ `;
124
+ /** Future migrations appended here in order (v1 is baseline — empty). */
125
+ export const migrations = [];
126
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/storage/schema.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0HrB,CAAC;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,UAAU,GAAa,EAAE,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { CurriculumService } from '../services/curriculum.js';
3
+ import type { SessionState } from '../types.js';
4
+ export declare function registerCurriculumTools(server: McpServer, svc: CurriculumService, sessions: Map<string, SessionState>, notify: () => void): void;