@gonzih/story-teacher 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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/SKILL.md +40 -0
  4. package/dist/adaptive.d.ts +13 -0
  5. package/dist/adaptive.js +88 -0
  6. package/dist/claude.d.ts +16 -0
  7. package/dist/claude.js +88 -0
  8. package/dist/database.d.ts +43 -0
  9. package/dist/database.js +133 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +6 -0
  12. package/dist/server.d.ts +2 -0
  13. package/dist/server.js +429 -0
  14. package/dist/stories/coding-quest.d.ts +2 -0
  15. package/dist/stories/coding-quest.js +76 -0
  16. package/dist/stories/eco-explorer.d.ts +2 -0
  17. package/dist/stories/eco-explorer.js +76 -0
  18. package/dist/stories/index.d.ts +8 -0
  19. package/dist/stories/index.js +37 -0
  20. package/dist/stories/number-kingdom.d.ts +2 -0
  21. package/dist/stories/number-kingdom.js +123 -0
  22. package/dist/stories/science-ship.d.ts +2 -0
  23. package/dist/stories/science-ship.js +77 -0
  24. package/dist/stories/time-traveler.d.ts +2 -0
  25. package/dist/stories/time-traveler.js +79 -0
  26. package/dist/stories/types.d.ts +43 -0
  27. package/dist/stories/types.js +1 -0
  28. package/dist/stories/word-wizard.d.ts +2 -0
  29. package/dist/stories/word-wizard.js +76 -0
  30. package/llms.txt +35 -0
  31. package/package.json +30 -0
  32. package/src/adaptive.ts +111 -0
  33. package/src/claude.ts +120 -0
  34. package/src/database.ts +185 -0
  35. package/src/index.ts +7 -0
  36. package/src/server.ts +533 -0
  37. package/src/stories/coding-quest.ts +79 -0
  38. package/src/stories/eco-explorer.ts +79 -0
  39. package/src/stories/index.ts +47 -0
  40. package/src/stories/number-kingdom.ts +128 -0
  41. package/src/stories/science-ship.ts +80 -0
  42. package/src/stories/time-traveler.ts +84 -0
  43. package/src/stories/types.ts +47 -0
  44. package/src/stories/word-wizard.ts +79 -0
  45. package/tsconfig.json +15 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maksim Soltan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # story-teacher
2
+
3
+ An MCP server that wraps AI conversations with children aged 5–12 in interactive, educational stories. Learning objectives (math, science, history, vocabulary, coding, ecology) are embedded in choose-your-own-adventure narratives powered by Claude.
4
+
5
+ ## Features
6
+
7
+ - **6 complete story worlds** with 30+ story segments
8
+ - **Adaptive difficulty** that never punishes wrong answers — always redirects through narrative
9
+ - **Claude-powered** dynamic story continuations with streaming
10
+ - **SQLite session tracking** with parent progress reports
11
+ - **Curriculum-mapped** with Common Core and NGSS standards
12
+ - **MCP-compatible** — works with Claude Desktop and any MCP client
13
+
14
+ ## Story Worlds
15
+
16
+ | Story | Subject | Ages |
17
+ |-------|---------|------|
18
+ | The Number Kingdom | Math | 5–12 |
19
+ | The Science Ship | Science | 7–11 |
20
+ | The Time Traveler | History | 8–12 |
21
+ | The Word Wizard | Literacy | 6–10 |
22
+ | The Coding Quest | Coding | 9–12 |
23
+ | The Eco Explorer | Ecology | 7–11 |
24
+
25
+ ## MCP Tools
26
+
27
+ ### `start_story`
28
+ Begin a new story session.
29
+ ```json
30
+ {
31
+ "profileId": "child-name",
32
+ "ageGroup": "8-10",
33
+ "storyType": "number-kingdom"
34
+ }
35
+ ```
36
+
37
+ ### `answer`
38
+ Submit an answer to the current question.
39
+ ```json
40
+ {
41
+ "sessionId": "...",
42
+ "answer": "56"
43
+ }
44
+ ```
45
+
46
+ ### `get_story_types`
47
+ List all available story types.
48
+
49
+ ### `get_session_summary`
50
+ Get what was learned in a session — great for parents.
51
+
52
+ ### `get_progress`
53
+ View overall learning progress, streaks, and points for a profile.
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ npm install -g @gonzih/story-teacher
59
+ ```
60
+
61
+ Requires `ANTHROPIC_API_KEY` environment variable.
62
+
63
+ ## Claude Desktop Config
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "story-teacher": {
69
+ "command": "story-teacher",
70
+ "env": {
71
+ "ANTHROPIC_API_KEY": "your-key-here"
72
+ }
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ ## Development
79
+
80
+ ```bash
81
+ npm install
82
+ npm run build
83
+ npm start
84
+ ```
85
+
86
+ ## License
87
+
88
+ MIT — Gonzih
package/SKILL.md ADDED
@@ -0,0 +1,40 @@
1
+ # story-teacher Skill
2
+
3
+ ## What it does
4
+
5
+ Runs educational storytelling sessions for children aged 5–12 via MCP. Each session is a choose-your-own-adventure story where learning questions are embedded naturally in the narrative. Correct answers advance the story dramatically; wrong answers get a gentle narrative redirect (never "wrong").
6
+
7
+ ## How to use
8
+
9
+ 1. Call `get_story_types` to show the user which adventures are available
10
+ 2. Call `start_story` with the child's profile ID, age group, and chosen story
11
+ 3. Present the `storyIntro` and `firstQuestion.text` to the child
12
+ 4. When the child responds, call `answer` with the session ID and their answer
13
+ 5. Present `storyContinuation` and `nextQuestion.text`
14
+ 6. Repeat until the child is done, then call `get_session_summary`
15
+
16
+ ## Age groups
17
+
18
+ - `5-7` — Very young; simple vocabulary, counting, basic addition/subtraction
19
+ - `8-10` — Elementary; multiplication, fractions, basic science, history
20
+ - `11-12` — Pre-teen; algebra, complex science, deeper history, coding logic
21
+
22
+ ## Example session
23
+
24
+ ```
25
+ Parent: Start a math story for my 7-year-old named Sam
26
+ → start_story({ profileId: "sam", ageGroup: "5-7", storyType: "number-kingdom" })
27
+
28
+ Child: [reads intro and question]
29
+ Child: "8"
30
+ → answer({ sessionId: "...", answer: "8" })
31
+ → Returns: correct=true, story celebration, next question
32
+ ```
33
+
34
+ ## Points system
35
+
36
+ - Easy correct: 10 points
37
+ - Medium correct: 15 points
38
+ - Hard correct: 25 points
39
+ - Streak bonus (3+ correct): 1.2×
40
+ - Milestone messages every 5 correct answers
@@ -0,0 +1,13 @@
1
+ import { StorySegment } from './stories/types.js';
2
+ export interface DifficultyState {
3
+ consecutiveWrong: number;
4
+ consecutiveCorrect: number;
5
+ totalCorrect: number;
6
+ totalAnswered: number;
7
+ usedSegmentIds: string[];
8
+ }
9
+ export declare function selectNextSegment(state: DifficultyState, storyType: string, ageGroup: string): StorySegment | null;
10
+ export declare function shouldAddExtraHint(state: DifficultyState): boolean;
11
+ export declare function calculatePoints(correct: boolean, difficulty: string, consecutiveCorrect: number): number;
12
+ export declare function getMilestoneMessage(questionsAnswered: number): string | null;
13
+ export declare function getEncouragementAfterWrong(consecutiveWrong: number): string;
@@ -0,0 +1,88 @@
1
+ import { getSegmentsForAgeGroup } from './stories/index.js';
2
+ export function selectNextSegment(state, storyType, ageGroup) {
3
+ const allSegments = getSegmentsForAgeGroup(storyType, ageGroup);
4
+ if (allSegments.length === 0)
5
+ return null;
6
+ // Filter out already used segments
7
+ const available = allSegments.filter(s => !state.usedSegmentIds.includes(s.id));
8
+ if (available.length === 0) {
9
+ // Recycle segments when exhausted (allow reuse)
10
+ const recycled = allSegments.filter(s => state.usedSegmentIds.slice(-Math.floor(allSegments.length / 2)).indexOf(s.id) === -1);
11
+ if (recycled.length === 0)
12
+ return allSegments[Math.floor(Math.random() * allSegments.length)];
13
+ return pickByDifficulty(recycled, state);
14
+ }
15
+ return pickByDifficulty(available, state);
16
+ }
17
+ function pickByDifficulty(segments, state) {
18
+ let preferredDifficulty;
19
+ if (state.consecutiveWrong >= 3) {
20
+ // Struggling — go easier
21
+ preferredDifficulty = 'easy';
22
+ }
23
+ else if (state.consecutiveCorrect >= 5) {
24
+ // On a roll — challenge them
25
+ preferredDifficulty = 'hard';
26
+ }
27
+ else if (state.totalAnswered > 0 && state.totalCorrect / state.totalAnswered > 0.8) {
28
+ // Doing well — medium/hard
29
+ preferredDifficulty = 'medium';
30
+ }
31
+ else {
32
+ preferredDifficulty = 'medium';
33
+ }
34
+ // Try to get preferred difficulty
35
+ const preferred = segments.filter(s => s.difficulty === preferredDifficulty);
36
+ if (preferred.length > 0) {
37
+ return preferred[Math.floor(Math.random() * preferred.length)];
38
+ }
39
+ // Fall back to any difficulty, biased toward preferred
40
+ const easier = ['easy', 'medium', 'hard'];
41
+ if (state.consecutiveWrong >= 3) {
42
+ for (const diff of easier) {
43
+ const found = segments.filter(s => s.difficulty === diff);
44
+ if (found.length > 0)
45
+ return found[Math.floor(Math.random() * found.length)];
46
+ }
47
+ }
48
+ // Return random from available
49
+ return segments[Math.floor(Math.random() * segments.length)];
50
+ }
51
+ export function shouldAddExtraHint(state) {
52
+ return state.consecutiveWrong >= 3;
53
+ }
54
+ export function calculatePoints(correct, difficulty, consecutiveCorrect) {
55
+ if (!correct)
56
+ return 0;
57
+ let base = 10;
58
+ if (difficulty === 'medium')
59
+ base = 15;
60
+ if (difficulty === 'hard')
61
+ base = 25;
62
+ // Streak bonus
63
+ if (consecutiveCorrect >= 5)
64
+ base = Math.floor(base * 1.5);
65
+ else if (consecutiveCorrect >= 3)
66
+ base = Math.floor(base * 1.2);
67
+ return base;
68
+ }
69
+ export function getMilestoneMessage(questionsAnswered) {
70
+ const milestones = {
71
+ 5: "AMAZING! You've answered 5 questions! The whole kingdom is celebrating your brilliance! Keep going — great adventures lie ahead!",
72
+ 10: "TEN QUESTIONS! You are a true champion! The story-world is glowing with your achievements! You're unstoppable!",
73
+ 15: "FIFTEEN! You've become a legend in this world! Ancient records are being updated to include YOUR name! What a journey!",
74
+ 20: "TWENTY QUESTIONS — you've mastered the realm! The greatest adventurers who ever lived couldn't have done better!",
75
+ };
76
+ return milestones[questionsAnswered] ?? null;
77
+ }
78
+ export function getEncouragementAfterWrong(consecutiveWrong) {
79
+ const messages = [
80
+ "Almost! Let's think about this together...",
81
+ "Great try! Every explorer gets stuck sometimes — that's how we learn!",
82
+ "You're so close! Let me give you a helpful clue...",
83
+ "Don't worry at all — even the wisest wizards need hints sometimes!",
84
+ "The path forward becomes clearer with each step — here's another clue!",
85
+ ];
86
+ const idx = Math.min(consecutiveWrong - 1, messages.length - 1);
87
+ return messages[idx];
88
+ }
@@ -0,0 +1,16 @@
1
+ export interface StoryContext {
2
+ storyType: string;
3
+ storyName: string;
4
+ ageGroup: string;
5
+ currentSegmentIntro: string;
6
+ question: string;
7
+ userAnswer: string;
8
+ isCorrect: boolean;
9
+ correctContinuation: string;
10
+ incorrectContinuation: string;
11
+ consecutiveWrong: number;
12
+ questionsAnswered: number;
13
+ characterName?: string;
14
+ }
15
+ export declare function generateStoryContinuation(context: StoryContext): Promise<string>;
16
+ export declare function generateStoryIntro(storyType: string, storyName: string, ageGroup: string, segmentIntro: string, firstQuestion: string): Promise<string>;
package/dist/claude.js ADDED
@@ -0,0 +1,88 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ const client = new Anthropic();
3
+ export async function generateStoryContinuation(context) {
4
+ const ageDescriptor = context.ageGroup === '5-7' ? 'ages 5-7 (very young, simple vocabulary)' :
5
+ context.ageGroup === '8-10' ? 'ages 8-10 (elementary school, curious and energetic)' :
6
+ 'ages 11-12 (pre-teen, sophisticated, loves challenges)';
7
+ const systemPrompt = `You are a magical storyteller for children aged ${context.ageGroup}. You are narrating "${context.storyName}".
8
+ Your voice is warm, exciting, and encouraging. You NEVER break story immersion.
9
+ When a child answers correctly: celebrate with the story, make them feel heroic and brilliant!
10
+ When a child answers incorrectly: gently redirect through the narrative — the story pauses briefly, a character helps, but you NEVER say "wrong" or "incorrect". Keep it narrative: "Hmm, the door doesn't open yet..." or "The spell fizzles a little..."
11
+ Keep responses SHORT (2-4 sentences). Age-appropriate vocabulary for ${ageDescriptor}. No scary or dark themes. Pure adventure, wonder, and encouragement.
12
+ Always end on an exciting note that makes the child want to keep going.`;
13
+ const userMessage = context.isCorrect
14
+ ? `The child is in the story: "${context.storyType}".
15
+ The current scene: ${context.currentSegmentIntro}
16
+ The question was: "${context.question}"
17
+ The child answered: "${context.userAnswer}" — which is CORRECT!
18
+ The template continuation is: "${context.correctContinuation}"
19
+ Generate an exciting, celebratory 2-4 sentence story continuation in this world. Make the child feel like a hero! Be creative and add vivid sensory details.`
20
+ : `The child is in the story: "${context.storyType}".
21
+ The current scene: ${context.currentSegmentIntro}
22
+ The question was: "${context.question}"
23
+ The child answered: "${context.userAnswer}" — which needs another try.
24
+ ${context.consecutiveWrong >= 3 ? 'This child has tried several times — be extra gentle and hint very directly through the story.' : ''}
25
+ The template continuation is: "${context.incorrectContinuation}"
26
+ Generate a kind, narrative 2-4 sentence story continuation that gently redirects without breaking immersion. Never say "wrong". A story character can offer a clue.`;
27
+ try {
28
+ let fullText = '';
29
+ const stream = await client.messages.create({
30
+ model: 'claude-opus-4-6',
31
+ max_tokens: 16000,
32
+ thinking: {
33
+ type: 'enabled',
34
+ budget_tokens: 8000,
35
+ },
36
+ system: systemPrompt,
37
+ messages: [{ role: 'user', content: userMessage }],
38
+ stream: true,
39
+ });
40
+ for await (const event of stream) {
41
+ if (event.type === 'content_block_delta') {
42
+ if (event.delta.type === 'text_delta') {
43
+ fullText += event.delta.text;
44
+ }
45
+ }
46
+ }
47
+ return fullText.trim() || (context.isCorrect ? context.correctContinuation : context.incorrectContinuation);
48
+ }
49
+ catch (error) {
50
+ // Fallback to template text if Claude fails
51
+ console.error('Claude generation failed, using template:', error);
52
+ return context.isCorrect ? context.correctContinuation : context.incorrectContinuation;
53
+ }
54
+ }
55
+ export async function generateStoryIntro(storyType, storyName, ageGroup, segmentIntro, firstQuestion) {
56
+ const systemPrompt = `You are a magical storyteller for children aged ${ageGroup}. You are narrating "${storyName}".
57
+ Your voice is warm, inviting, and full of wonder. Write a captivating opening that draws the child immediately into the adventure.
58
+ Keep it to 3-4 sentences. Age-appropriate. No scary themes. Pure excitement and possibility!`;
59
+ try {
60
+ let fullText = '';
61
+ const stream = await client.messages.create({
62
+ model: 'claude-opus-4-6',
63
+ max_tokens: 8000,
64
+ thinking: {
65
+ type: 'enabled',
66
+ budget_tokens: 4000,
67
+ },
68
+ system: systemPrompt,
69
+ messages: [{
70
+ role: 'user',
71
+ content: `Generate a captivating 3-4 sentence story introduction based on this template: "${segmentIntro}". End with a hint of the adventure ahead. The first challenge will involve: "${firstQuestion}". Make it irresistible!`,
72
+ }],
73
+ stream: true,
74
+ });
75
+ for await (const event of stream) {
76
+ if (event.type === 'content_block_delta') {
77
+ if (event.delta.type === 'text_delta') {
78
+ fullText += event.delta.text;
79
+ }
80
+ }
81
+ }
82
+ return fullText.trim() || segmentIntro;
83
+ }
84
+ catch (error) {
85
+ console.error('Claude intro generation failed, using template:', error);
86
+ return segmentIntro;
87
+ }
88
+ }
@@ -0,0 +1,43 @@
1
+ import Database from 'better-sqlite3';
2
+ export declare function getDatabase(): Database.Database;
3
+ export interface SessionRow {
4
+ id: string;
5
+ profileId: string;
6
+ storyType: string;
7
+ ageGroup: string;
8
+ startedAt: number;
9
+ lastActivityAt: number;
10
+ questionsAnswered: number;
11
+ correctAnswers: number;
12
+ totalPoints: number;
13
+ conceptsCovered: string;
14
+ isComplete: number;
15
+ }
16
+ export interface AnswerRow {
17
+ id: string;
18
+ sessionId: string;
19
+ question: string;
20
+ userAnswer: string;
21
+ correct: number;
22
+ concept: string;
23
+ subject: string;
24
+ gradeLevel: number;
25
+ pointsEarned: number;
26
+ timestamp: number;
27
+ }
28
+ export interface ProfileRow {
29
+ id: string;
30
+ totalSessions: number;
31
+ totalPoints: number;
32
+ lastSessionAt: number | null;
33
+ streakDays: number;
34
+ }
35
+ export declare function createSession(session: Omit<SessionRow, 'questionsAnswered' | 'correctAnswers' | 'totalPoints' | 'conceptsCovered' | 'isComplete'>): void;
36
+ export declare function getSession(sessionId: string): SessionRow | undefined;
37
+ export declare function updateSession(sessionId: string, updates: Partial<Omit<SessionRow, 'id'>>): void;
38
+ export declare function recordAnswer(answer: AnswerRow): void;
39
+ export declare function getSessionAnswers(sessionId: string): AnswerRow[];
40
+ export declare function getOrCreateProfile(profileId: string): ProfileRow;
41
+ export declare function updateProfile(profileId: string, updates: Partial<Omit<ProfileRow, 'id'>>): void;
42
+ export declare function getProfileSessions(profileId: string): SessionRow[];
43
+ export declare function calculateStreakDays(profileId: string): number;
@@ -0,0 +1,133 @@
1
+ import Database from 'better-sqlite3';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { mkdirSync } from 'fs';
5
+ const DB_DIR = join(homedir(), '.story-teacher');
6
+ const DB_PATH = join(DB_DIR, 'story-teacher.db');
7
+ let db;
8
+ export function getDatabase() {
9
+ if (!db) {
10
+ mkdirSync(DB_DIR, { recursive: true });
11
+ db = new Database(DB_PATH);
12
+ db.pragma('journal_mode = WAL');
13
+ initializeSchema(db);
14
+ }
15
+ return db;
16
+ }
17
+ function initializeSchema(database) {
18
+ database.exec(`
19
+ CREATE TABLE IF NOT EXISTS sessions (
20
+ id TEXT PRIMARY KEY,
21
+ profileId TEXT NOT NULL,
22
+ storyType TEXT NOT NULL,
23
+ ageGroup TEXT NOT NULL,
24
+ startedAt INTEGER NOT NULL,
25
+ lastActivityAt INTEGER NOT NULL,
26
+ questionsAnswered INTEGER DEFAULT 0,
27
+ correctAnswers INTEGER DEFAULT 0,
28
+ totalPoints INTEGER DEFAULT 0,
29
+ conceptsCovered TEXT DEFAULT '[]',
30
+ isComplete INTEGER DEFAULT 0
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS answers (
34
+ id TEXT PRIMARY KEY,
35
+ sessionId TEXT NOT NULL,
36
+ question TEXT NOT NULL,
37
+ userAnswer TEXT NOT NULL,
38
+ correct INTEGER NOT NULL,
39
+ concept TEXT NOT NULL,
40
+ subject TEXT NOT NULL,
41
+ gradeLevel INTEGER NOT NULL,
42
+ pointsEarned INTEGER NOT NULL,
43
+ timestamp INTEGER NOT NULL
44
+ );
45
+
46
+ CREATE TABLE IF NOT EXISTS profiles (
47
+ id TEXT PRIMARY KEY,
48
+ totalSessions INTEGER DEFAULT 0,
49
+ totalPoints INTEGER DEFAULT 0,
50
+ lastSessionAt INTEGER,
51
+ streakDays INTEGER DEFAULT 0
52
+ );
53
+
54
+ CREATE INDEX IF NOT EXISTS idx_sessions_profileId ON sessions(profileId);
55
+ CREATE INDEX IF NOT EXISTS idx_answers_sessionId ON answers(sessionId);
56
+ `);
57
+ }
58
+ export function createSession(session) {
59
+ const database = getDatabase();
60
+ database.prepare(`
61
+ INSERT INTO sessions (id, profileId, storyType, ageGroup, startedAt, lastActivityAt)
62
+ VALUES (?, ?, ?, ?, ?, ?)
63
+ `).run(session.id, session.profileId, session.storyType, session.ageGroup, session.startedAt, session.lastActivityAt);
64
+ }
65
+ export function getSession(sessionId) {
66
+ const database = getDatabase();
67
+ return database.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
68
+ }
69
+ export function updateSession(sessionId, updates) {
70
+ const database = getDatabase();
71
+ const fields = Object.keys(updates).map(k => `${k} = ?`).join(', ');
72
+ const values = [...Object.values(updates), sessionId];
73
+ database.prepare(`UPDATE sessions SET ${fields} WHERE id = ?`).run(...values);
74
+ }
75
+ export function recordAnswer(answer) {
76
+ const database = getDatabase();
77
+ database.prepare(`
78
+ INSERT INTO answers (id, sessionId, question, userAnswer, correct, concept, subject, gradeLevel, pointsEarned, timestamp)
79
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
80
+ `).run(answer.id, answer.sessionId, answer.question, answer.userAnswer, answer.correct, answer.concept, answer.subject, answer.gradeLevel, answer.pointsEarned, answer.timestamp);
81
+ }
82
+ export function getSessionAnswers(sessionId) {
83
+ const database = getDatabase();
84
+ return database.prepare('SELECT * FROM answers WHERE sessionId = ? ORDER BY timestamp ASC').all(sessionId);
85
+ }
86
+ export function getOrCreateProfile(profileId) {
87
+ const database = getDatabase();
88
+ const existing = database.prepare('SELECT * FROM profiles WHERE id = ?').get(profileId);
89
+ if (existing)
90
+ return existing;
91
+ database.prepare(`
92
+ INSERT INTO profiles (id, totalSessions, totalPoints, lastSessionAt, streakDays)
93
+ VALUES (?, 0, 0, NULL, 0)
94
+ `).run(profileId);
95
+ return database.prepare('SELECT * FROM profiles WHERE id = ?').get(profileId);
96
+ }
97
+ export function updateProfile(profileId, updates) {
98
+ const database = getDatabase();
99
+ const fields = Object.keys(updates).map(k => `${k} = ?`).join(', ');
100
+ const values = [...Object.values(updates), profileId];
101
+ database.prepare(`UPDATE profiles SET ${fields} WHERE id = ?`).run(...values);
102
+ }
103
+ export function getProfileSessions(profileId) {
104
+ const database = getDatabase();
105
+ return database.prepare('SELECT * FROM sessions WHERE profileId = ? ORDER BY startedAt DESC').all(profileId);
106
+ }
107
+ export function calculateStreakDays(profileId) {
108
+ const database = getDatabase();
109
+ const sessions = database.prepare(`
110
+ SELECT date(startedAt / 1000, 'unixepoch') as day
111
+ FROM sessions
112
+ WHERE profileId = ?
113
+ GROUP BY day
114
+ ORDER BY day DESC
115
+ `).all(profileId);
116
+ if (sessions.length === 0)
117
+ return 0;
118
+ let streak = 0;
119
+ const today = new Date();
120
+ today.setHours(0, 0, 0, 0);
121
+ for (let i = 0; i < sessions.length; i++) {
122
+ const sessionDate = new Date(sessions[i].day);
123
+ const expectedDate = new Date(today);
124
+ expectedDate.setDate(today.getDate() - i);
125
+ if (sessionDate.toDateString() === expectedDate.toDateString()) {
126
+ streak++;
127
+ }
128
+ else {
129
+ break;
130
+ }
131
+ }
132
+ return streak;
133
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { createServer } from './server.js';
4
+ const server = createServer();
5
+ const transport = new StdioServerTransport();
6
+ await server.connect(transport);
@@ -0,0 +1,2 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ export declare function createServer(): Server;