connectry-architect-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/dist/index.js ADDED
@@ -0,0 +1,2641 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/db/store.ts
8
+ import Database from "better-sqlite3";
9
+ import fs from "fs";
10
+ import path from "path";
11
+
12
+ // src/db/schema.ts
13
+ var SCHEMA_SQL = `
14
+ CREATE TABLE IF NOT EXISTS users (
15
+ id TEXT PRIMARY KEY,
16
+ displayName TEXT,
17
+ createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
18
+ lastActivityAt DATETIME,
19
+ assessmentCompleted BOOLEAN DEFAULT FALSE,
20
+ learningPath TEXT
21
+ );
22
+
23
+ CREATE TABLE IF NOT EXISTS domain_mastery (
24
+ userId TEXT NOT NULL,
25
+ taskStatement TEXT NOT NULL,
26
+ domainId INTEGER NOT NULL,
27
+ totalAttempts INTEGER DEFAULT 0,
28
+ correctAttempts INTEGER DEFAULT 0,
29
+ accuracyPercent REAL DEFAULT 0,
30
+ masteryLevel TEXT DEFAULT 'unassessed',
31
+ lastTestedAt DATETIME,
32
+ FOREIGN KEY (userId) REFERENCES users(id),
33
+ PRIMARY KEY (userId, taskStatement)
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS answers (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ userId TEXT NOT NULL,
39
+ questionId TEXT NOT NULL,
40
+ taskStatement TEXT NOT NULL,
41
+ domainId INTEGER NOT NULL,
42
+ userAnswer TEXT NOT NULL,
43
+ correctAnswer TEXT NOT NULL,
44
+ isCorrect BOOLEAN NOT NULL,
45
+ difficulty TEXT NOT NULL,
46
+ answeredAt DATETIME DEFAULT CURRENT_TIMESTAMP,
47
+ FOREIGN KEY (userId) REFERENCES users(id)
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS review_schedule (
51
+ userId TEXT NOT NULL,
52
+ taskStatement TEXT NOT NULL,
53
+ nextReviewAt DATETIME NOT NULL,
54
+ interval INTEGER DEFAULT 1,
55
+ easeFactor REAL DEFAULT 2.5,
56
+ consecutiveCorrect INTEGER DEFAULT 0,
57
+ FOREIGN KEY (userId) REFERENCES users(id),
58
+ PRIMARY KEY (userId, taskStatement)
59
+ );
60
+
61
+ CREATE TABLE IF NOT EXISTS study_sessions (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ userId TEXT NOT NULL,
64
+ startedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
65
+ endedAt DATETIME,
66
+ domainId INTEGER,
67
+ questionsAnswered INTEGER DEFAULT 0,
68
+ correctAnswers INTEGER DEFAULT 0,
69
+ mode TEXT,
70
+ FOREIGN KEY (userId) REFERENCES users(id)
71
+ );
72
+
73
+ CREATE TABLE IF NOT EXISTS handout_views (
74
+ userId TEXT NOT NULL,
75
+ taskStatement TEXT NOT NULL,
76
+ viewedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
77
+ timeSpentSeconds INTEGER DEFAULT 0,
78
+ PRIMARY KEY (userId, taskStatement),
79
+ FOREIGN KEY (userId) REFERENCES users(id)
80
+ );
81
+
82
+ CREATE TABLE IF NOT EXISTS session_state (
83
+ userId TEXT PRIMARY KEY,
84
+ currentMode TEXT NOT NULL,
85
+ currentDomain INTEGER,
86
+ currentTaskStatement TEXT,
87
+ currentQuestionIndex INTEGER DEFAULT 0,
88
+ positionStack TEXT DEFAULT '[]',
89
+ reviewQueueIds TEXT DEFAULT '[]',
90
+ lastUpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
91
+ FOREIGN KEY (userId) REFERENCES users(id)
92
+ );
93
+
94
+ CREATE TABLE IF NOT EXISTS exam_attempts (
95
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
96
+ userId TEXT NOT NULL,
97
+ startedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
98
+ completedAt DATETIME,
99
+ totalQuestions INTEGER NOT NULL DEFAULT 60,
100
+ correctAnswers INTEGER DEFAULT 0,
101
+ score INTEGER DEFAULT 0,
102
+ passed BOOLEAN DEFAULT FALSE,
103
+ questionIds TEXT NOT NULL DEFAULT '[]',
104
+ answeredQuestionIds TEXT NOT NULL DEFAULT '[]',
105
+ domainScores TEXT NOT NULL DEFAULT '{}',
106
+ FOREIGN KEY (userId) REFERENCES users(id)
107
+ );
108
+
109
+ CREATE TABLE IF NOT EXISTS capstone_builds (
110
+ id TEXT PRIMARY KEY,
111
+ userId TEXT NOT NULL,
112
+ theme TEXT NOT NULL,
113
+ currentStep INTEGER DEFAULT 0,
114
+ status TEXT DEFAULT 'shaping',
115
+ themeValidated INTEGER DEFAULT 0,
116
+ createdAt TEXT NOT NULL,
117
+ updatedAt TEXT NOT NULL,
118
+ FOREIGN KEY (userId) REFERENCES users(id)
119
+ );
120
+
121
+ CREATE TABLE IF NOT EXISTS capstone_build_steps (
122
+ id TEXT PRIMARY KEY,
123
+ buildId TEXT NOT NULL,
124
+ stepIndex INTEGER NOT NULL,
125
+ fileName TEXT NOT NULL,
126
+ taskStatements TEXT NOT NULL,
127
+ quizQuestionIds TEXT,
128
+ quizCompleted INTEGER DEFAULT 0,
129
+ buildCompleted INTEGER DEFAULT 0,
130
+ walkthroughViewed INTEGER DEFAULT 0,
131
+ createdAt TEXT NOT NULL,
132
+ updatedAt TEXT NOT NULL,
133
+ FOREIGN KEY (buildId) REFERENCES capstone_builds(id)
134
+ );
135
+ `;
136
+
137
+ // src/db/store.ts
138
+ function createDatabase(dbPath2) {
139
+ const db2 = new Database(dbPath2);
140
+ db2.pragma("journal_mode = WAL");
141
+ db2.pragma("foreign_keys = ON");
142
+ db2.pragma("synchronous = NORMAL");
143
+ db2.pragma("busy_timeout = 5000");
144
+ db2.exec(SCHEMA_SQL);
145
+ return db2;
146
+ }
147
+ function getDefaultDbPath() {
148
+ const dir = path.join(
149
+ process.env["HOME"] ?? process.env["USERPROFILE"] ?? ".",
150
+ ".connectry-architect"
151
+ );
152
+ fs.mkdirSync(dir, { recursive: true });
153
+ return path.join(dir, "progress.db");
154
+ }
155
+
156
+ // src/config.ts
157
+ import fs2 from "fs";
158
+ import path2 from "path";
159
+ import crypto from "crypto";
160
+ function getConfigDir() {
161
+ const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? ".";
162
+ return path2.join(home, ".connectry-architect");
163
+ }
164
+ function getConfigPath() {
165
+ return path2.join(getConfigDir(), "config.json");
166
+ }
167
+ function loadOrCreateUserConfig() {
168
+ const configPath = getConfigPath();
169
+ if (fs2.existsSync(configPath)) {
170
+ const raw = fs2.readFileSync(configPath, "utf-8");
171
+ return JSON.parse(raw);
172
+ }
173
+ const config = {
174
+ userId: crypto.randomUUID(),
175
+ displayName: null,
176
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
177
+ };
178
+ fs2.mkdirSync(getConfigDir(), { recursive: true });
179
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
180
+ return config;
181
+ }
182
+
183
+ // src/tools/submit-answer.ts
184
+ import { z } from "zod";
185
+
186
+ // src/engine/grading.ts
187
+ function gradeAnswer(question, userAnswer) {
188
+ const normalizedAnswer = userAnswer.toUpperCase();
189
+ const isCorrect = normalizedAnswer === question.correctAnswer;
190
+ return {
191
+ questionId: question.id,
192
+ isCorrect,
193
+ userAnswer: normalizedAnswer,
194
+ correctAnswer: question.correctAnswer,
195
+ explanation: question.explanation,
196
+ whyUserWasWrong: isCorrect ? null : question.whyWrongMap[normalizedAnswer] ?? null,
197
+ references: question.references
198
+ };
199
+ }
200
+
201
+ // src/engine/spaced-repetition.ts
202
+ function calculateSM2(input) {
203
+ const { isCorrect, previousInterval, previousEaseFactor, previousConsecutiveCorrect } = input;
204
+ if (!isCorrect) {
205
+ const newEase2 = Math.max(1.3, previousEaseFactor - 0.2);
206
+ const nextReviewAt2 = addDays(/* @__PURE__ */ new Date(), 1);
207
+ return { interval: 1, easeFactor: Math.round(newEase2 * 100) / 100, consecutiveCorrect: 0, nextReviewAt: nextReviewAt2.toISOString() };
208
+ }
209
+ const newConsecutive = previousConsecutiveCorrect + 1;
210
+ const newEase = Math.max(1.3, previousEaseFactor + 0.1);
211
+ let interval;
212
+ if (newConsecutive === 1) interval = 1;
213
+ else if (newConsecutive === 2) interval = 3;
214
+ else interval = Math.round(previousInterval * previousEaseFactor);
215
+ const nextReviewAt = addDays(/* @__PURE__ */ new Date(), interval);
216
+ return { interval, easeFactor: Math.round(newEase * 100) / 100, consecutiveCorrect: newConsecutive, nextReviewAt: nextReviewAt.toISOString() };
217
+ }
218
+ function addDays(date, days) {
219
+ const result = new Date(date);
220
+ result.setDate(result.getDate() + days);
221
+ return result;
222
+ }
223
+
224
+ // src/data/loader.ts
225
+ import fs3 from "fs";
226
+ import path3 from "path";
227
+ import { fileURLToPath } from "url";
228
+ var __dirname = path3.dirname(fileURLToPath(import.meta.url));
229
+ var cachedCurriculum = null;
230
+ var cachedQuestions = null;
231
+ function loadCurriculum() {
232
+ if (cachedCurriculum) return cachedCurriculum;
233
+ const raw = fs3.readFileSync(path3.join(__dirname, "curriculum.json"), "utf-8");
234
+ cachedCurriculum = JSON.parse(raw);
235
+ return cachedCurriculum;
236
+ }
237
+ function loadQuestions(domainId) {
238
+ if (!cachedQuestions) {
239
+ const questionsMap = /* @__PURE__ */ new Map();
240
+ for (let d = 1; d <= 5; d++) {
241
+ const filePath = path3.join(__dirname, "questions", `domain-${d}.json`);
242
+ if (fs3.existsSync(filePath)) {
243
+ const raw = fs3.readFileSync(filePath, "utf-8");
244
+ const bank = JSON.parse(raw);
245
+ questionsMap.set(d, bank.questions);
246
+ } else {
247
+ questionsMap.set(d, []);
248
+ }
249
+ }
250
+ cachedQuestions = questionsMap;
251
+ }
252
+ if (domainId !== void 0) return cachedQuestions.get(domainId) ?? [];
253
+ return Array.from(cachedQuestions.values()).flat();
254
+ }
255
+ function loadHandout(taskStatement) {
256
+ const filename = getHandoutFilename(taskStatement);
257
+ const filePath = path3.join(__dirname, "handouts", filename);
258
+ if (!fs3.existsSync(filePath)) return null;
259
+ return fs3.readFileSync(filePath, "utf-8");
260
+ }
261
+ function getHandoutFilename(taskStatement) {
262
+ const curriculum = loadCurriculum();
263
+ for (const domain of curriculum.domains) {
264
+ for (const ts of domain.taskStatements) {
265
+ if (ts.id === taskStatement) {
266
+ const slug = ts.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+$/, "");
267
+ return `${ts.id}-${slug}.md`;
268
+ }
269
+ }
270
+ }
271
+ return `${taskStatement}.md`;
272
+ }
273
+
274
+ // src/db/answers.ts
275
+ function recordAnswer(db2, userId, questionId, taskStatement, domainId, userAnswer, correctAnswer, isCorrect, difficulty) {
276
+ db2.prepare(`
277
+ INSERT INTO answers (userId, questionId, taskStatement, domainId, userAnswer, correctAnswer, isCorrect, difficulty)
278
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
279
+ `).run(userId, questionId, taskStatement, domainId, userAnswer, correctAnswer, isCorrect ? 1 : 0, difficulty);
280
+ }
281
+ function getAnswersByTaskStatement(db2, userId, taskStatement) {
282
+ return db2.prepare("SELECT * FROM answers WHERE userId = ? AND taskStatement = ? ORDER BY answeredAt DESC").all(userId, taskStatement);
283
+ }
284
+ function getAnsweredQuestionIds(db2, userId) {
285
+ const rows = db2.prepare("SELECT DISTINCT questionId FROM answers WHERE userId = ?").all(userId);
286
+ return new Set(rows.map((r) => r.questionId));
287
+ }
288
+ function getTotalStats(db2, userId) {
289
+ const row = db2.prepare("SELECT COUNT(*) as total, SUM(CASE WHEN isCorrect THEN 1 ELSE 0 END) as correct FROM answers WHERE userId = ?").get(userId);
290
+ return { total: row.total, correct: row.correct ?? 0 };
291
+ }
292
+
293
+ // src/db/mastery.ts
294
+ function getMastery(db2, userId, taskStatement) {
295
+ return db2.prepare("SELECT * FROM domain_mastery WHERE userId = ? AND taskStatement = ?").get(userId, taskStatement);
296
+ }
297
+ function getAllMastery(db2, userId) {
298
+ return db2.prepare("SELECT * FROM domain_mastery WHERE userId = ? ORDER BY domainId, taskStatement").all(userId);
299
+ }
300
+ function getWeakAreas(db2, userId, threshold = 70) {
301
+ return db2.prepare("SELECT * FROM domain_mastery WHERE userId = ? AND accuracyPercent < ? AND totalAttempts > 0 ORDER BY accuracyPercent ASC").all(userId, threshold);
302
+ }
303
+ function calculateMasteryLevel(accuracy, total, consecutiveCorrect) {
304
+ if (total === 0) return "unassessed";
305
+ if (accuracy >= 90 && total >= 5 && consecutiveCorrect >= 3) return "mastered";
306
+ if (accuracy >= 70) return "strong";
307
+ if (accuracy >= 50) return "developing";
308
+ return "weak";
309
+ }
310
+ function updateMastery(db2, userId, taskStatement, domainId, isCorrect, consecutiveCorrect) {
311
+ const existing = getMastery(db2, userId, taskStatement);
312
+ if (!existing) {
313
+ const accuracy = isCorrect ? 100 : 0;
314
+ const level = calculateMasteryLevel(accuracy, 1, isCorrect ? 1 : 0);
315
+ db2.prepare(`INSERT INTO domain_mastery (userId, taskStatement, domainId, totalAttempts, correctAttempts, accuracyPercent, masteryLevel, lastTestedAt) VALUES (?, ?, ?, 1, ?, ?, ?, CURRENT_TIMESTAMP)`).run(userId, taskStatement, domainId, isCorrect ? 1 : 0, accuracy, level);
316
+ } else {
317
+ const newTotal = existing.totalAttempts + 1;
318
+ const newCorrect = existing.correctAttempts + (isCorrect ? 1 : 0);
319
+ const accuracy = Math.round(newCorrect / newTotal * 100);
320
+ const level = calculateMasteryLevel(accuracy, newTotal, consecutiveCorrect);
321
+ db2.prepare(`UPDATE domain_mastery SET totalAttempts = ?, correctAttempts = ?, accuracyPercent = ?, masteryLevel = ?, lastTestedAt = CURRENT_TIMESTAMP WHERE userId = ? AND taskStatement = ?`).run(newTotal, newCorrect, accuracy, level, userId, taskStatement);
322
+ }
323
+ return getMastery(db2, userId, taskStatement);
324
+ }
325
+
326
+ // src/db/review-schedule.ts
327
+ function getReviewSchedule(db2, userId, taskStatement) {
328
+ return db2.prepare("SELECT * FROM review_schedule WHERE userId = ? AND taskStatement = ?").get(userId, taskStatement);
329
+ }
330
+ function getOverdueReviews(db2, userId) {
331
+ return db2.prepare("SELECT * FROM review_schedule WHERE userId = ? AND nextReviewAt <= CURRENT_TIMESTAMP ORDER BY nextReviewAt ASC").all(userId);
332
+ }
333
+ function upsertReviewSchedule(db2, userId, taskStatement, interval, easeFactor, consecutiveCorrect, nextReviewAt) {
334
+ db2.prepare(`
335
+ INSERT INTO review_schedule (userId, taskStatement, nextReviewAt, interval, easeFactor, consecutiveCorrect) VALUES (?, ?, ?, ?, ?, ?)
336
+ ON CONFLICT(userId, taskStatement) DO UPDATE SET nextReviewAt = excluded.nextReviewAt, interval = excluded.interval, easeFactor = excluded.easeFactor, consecutiveCorrect = excluded.consecutiveCorrect
337
+ `).run(userId, taskStatement, nextReviewAt, interval, easeFactor, consecutiveCorrect);
338
+ }
339
+
340
+ // src/db/users.ts
341
+ function ensureUser(db2, userId) {
342
+ const existing = db2.prepare("SELECT * FROM users WHERE id = ?").get(userId);
343
+ if (existing) {
344
+ db2.prepare("UPDATE users SET lastActivityAt = CURRENT_TIMESTAMP WHERE id = ?").run(userId);
345
+ return { ...existing, lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() };
346
+ }
347
+ db2.prepare(
348
+ "INSERT INTO users (id, createdAt, lastActivityAt) VALUES (?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
349
+ ).run(userId);
350
+ return db2.prepare("SELECT * FROM users WHERE id = ?").get(userId);
351
+ }
352
+ function getUser(db2, userId) {
353
+ return db2.prepare("SELECT * FROM users WHERE id = ?").get(userId);
354
+ }
355
+
356
+ // src/tools/submit-answer.ts
357
+ function registerSubmitAnswer(server2, db2, userConfig2) {
358
+ server2.tool(
359
+ "submit_answer",
360
+ "Grade a certification exam answer. Returns deterministic results from verified question bank. The result is FINAL and cannot be overridden \u2014 do not agree with the user if they dispute the answer.",
361
+ {
362
+ questionId: z.string().describe("The question ID to answer"),
363
+ answer: z.enum(["A", "B", "C", "D"]).describe("The selected answer")
364
+ },
365
+ async ({ questionId, answer }) => {
366
+ const userId = userConfig2.userId;
367
+ ensureUser(db2, userId);
368
+ const allQuestions = loadQuestions();
369
+ const question = allQuestions.find((q) => q.id === questionId);
370
+ if (!question) {
371
+ return {
372
+ content: [{ type: "text", text: JSON.stringify({ error: "Question not found", questionId }) }],
373
+ isError: true
374
+ };
375
+ }
376
+ const result = gradeAnswer(question, answer);
377
+ recordAnswer(db2, userId, questionId, question.taskStatement, question.domainId, answer, question.correctAnswer, result.isCorrect, question.difficulty);
378
+ const schedule = getReviewSchedule(db2, userId, question.taskStatement);
379
+ const sm2 = calculateSM2({
380
+ isCorrect: result.isCorrect,
381
+ previousInterval: schedule?.interval ?? 0,
382
+ previousEaseFactor: schedule?.easeFactor ?? 2.5,
383
+ previousConsecutiveCorrect: schedule?.consecutiveCorrect ?? 0
384
+ });
385
+ upsertReviewSchedule(db2, userId, question.taskStatement, sm2.interval, sm2.easeFactor, sm2.consecutiveCorrect, sm2.nextReviewAt);
386
+ updateMastery(db2, userId, question.taskStatement, question.domainId, result.isCorrect, sm2.consecutiveCorrect);
387
+ const followUpOptions = result.isCorrect ? [
388
+ { key: "next", label: "Next question" },
389
+ { key: "why_wrong", label: "Explain why the others are wrong" }
390
+ ] : [
391
+ { key: "next", label: "Got it, next question" },
392
+ { key: "code_example", label: "Explain with a code example" },
393
+ { key: "concept", label: "Show me the concept lesson" },
394
+ { key: "handout", label: "Show me the handout" },
395
+ { key: "project", label: "Show me in the reference project" }
396
+ ];
397
+ const response = {
398
+ questionId: result.questionId,
399
+ isCorrect: result.isCorrect,
400
+ correctAnswer: result.correctAnswer,
401
+ explanation: result.explanation,
402
+ whyYourAnswerWasWrong: result.whyUserWasWrong,
403
+ references: result.references,
404
+ followUpOptions
405
+ };
406
+ return {
407
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
408
+ };
409
+ }
410
+ );
411
+ }
412
+
413
+ // src/tools/get-progress.ts
414
+ function registerGetProgress(server2, db2, userConfig2) {
415
+ server2.tool(
416
+ "get_progress",
417
+ "Get your certification study progress overview including mastery levels, accuracy, and review status.",
418
+ {},
419
+ async () => {
420
+ const userId = userConfig2.userId;
421
+ ensureUser(db2, userId);
422
+ const curriculum = loadCurriculum();
423
+ const mastery = getAllMastery(db2, userId);
424
+ const stats = getTotalStats(db2, userId);
425
+ const overdueReviews = getOverdueReviews(db2, userId);
426
+ const domainProgress = curriculum.domains.map((d) => {
427
+ const domainMastery = mastery.filter((m) => m.domainId === d.id);
428
+ const avgAccuracy = domainMastery.length > 0 ? Math.round(domainMastery.reduce((sum, m) => sum + m.accuracyPercent, 0) / domainMastery.length) : 0;
429
+ const masteredCount = domainMastery.filter((m) => m.masteryLevel === "mastered").length;
430
+ return ` D${d.id}: ${d.title} \u2014 ${avgAccuracy}% accuracy, ${masteredCount}/${d.taskStatements.length} mastered`;
431
+ });
432
+ const overallAccuracy = stats.total > 0 ? Math.round(stats.correct / stats.total * 100) : 0;
433
+ const text = [
434
+ "\u2550\u2550\u2550 CERTIFICATION STUDY PROGRESS \u2550\u2550\u2550",
435
+ "",
436
+ `Questions Answered: ${stats.total}`,
437
+ `Overall Accuracy: ${overallAccuracy}%`,
438
+ `Reviews Due: ${overdueReviews.length}`,
439
+ "",
440
+ "Domain Progress:",
441
+ ...domainProgress
442
+ ].join("\n");
443
+ return { content: [{ type: "text", text }] };
444
+ }
445
+ );
446
+ }
447
+
448
+ // src/tools/get-curriculum.ts
449
+ function registerGetCurriculum(server2, db2, userConfig2) {
450
+ server2.tool(
451
+ "get_curriculum",
452
+ "View the full certification curriculum with domains, task statements, and your current mastery for each.",
453
+ {},
454
+ async () => {
455
+ const userId = userConfig2.userId;
456
+ ensureUser(db2, userId);
457
+ const curriculum = loadCurriculum();
458
+ const mastery = getAllMastery(db2, userId);
459
+ const lines = ["\u2550\u2550\u2550 CERTIFICATION CURRICULUM \u2550\u2550\u2550", ""];
460
+ for (const domain of curriculum.domains) {
461
+ lines.push(`## Domain ${domain.id}: ${domain.title} (${domain.weight}%)`);
462
+ lines.push("");
463
+ for (const ts of domain.taskStatements) {
464
+ const m = mastery.find((x) => x.taskStatement === ts.id);
465
+ const level = m ? m.masteryLevel : "unassessed";
466
+ const acc = m ? `${m.accuracyPercent}%` : "\u2014";
467
+ lines.push(` ${ts.id} [${level.toUpperCase()}] ${ts.title} (${acc})`);
468
+ }
469
+ lines.push("");
470
+ }
471
+ return { content: [{ type: "text", text: lines.join("\n") }] };
472
+ }
473
+ );
474
+ }
475
+
476
+ // src/tools/get-section-details.ts
477
+ import { z as z2 } from "zod";
478
+
479
+ // src/db/handout-views.ts
480
+ function recordHandoutView(db2, userId, taskStatement) {
481
+ db2.prepare(`INSERT INTO handout_views (userId, taskStatement) VALUES (?, ?) ON CONFLICT(userId, taskStatement) DO UPDATE SET viewedAt = CURRENT_TIMESTAMP`).run(userId, taskStatement);
482
+ }
483
+ function hasViewedHandout(db2, userId, taskStatement) {
484
+ const row = db2.prepare("SELECT 1 FROM handout_views WHERE userId = ? AND taskStatement = ?").get(userId, taskStatement);
485
+ return row !== void 0;
486
+ }
487
+
488
+ // src/tools/get-section-details.ts
489
+ function registerGetSectionDetails(server2, db2, userConfig2) {
490
+ server2.tool(
491
+ "get_section_details",
492
+ "Get detailed information about a specific task statement including concept lesson, mastery, and history.",
493
+ { taskStatement: z2.string().describe('Task statement ID, e.g. "1.1"') },
494
+ async ({ taskStatement }) => {
495
+ const userId = userConfig2.userId;
496
+ ensureUser(db2, userId);
497
+ const curriculum = loadCurriculum();
498
+ let found = null;
499
+ for (const d of curriculum.domains) {
500
+ for (const ts of d.taskStatements) {
501
+ if (ts.id === taskStatement) {
502
+ found = { domain: d, ts };
503
+ break;
504
+ }
505
+ }
506
+ if (found) break;
507
+ }
508
+ if (!found) {
509
+ return {
510
+ content: [{ type: "text", text: JSON.stringify({ error: "Task statement not found", taskStatement }) }],
511
+ isError: true
512
+ };
513
+ }
514
+ const mastery = getMastery(db2, userId, taskStatement);
515
+ const answers = getAnswersByTaskStatement(db2, userId, taskStatement);
516
+ const handoutViewed = hasViewedHandout(db2, userId, taskStatement);
517
+ const handout = loadHandout(taskStatement);
518
+ const lines = [
519
+ `\u2550\u2550\u2550 ${found.ts.id}: ${found.ts.title} \u2550\u2550\u2550`,
520
+ `Domain: ${found.domain.title}`,
521
+ `Description: ${found.ts.description}`,
522
+ "",
523
+ `Mastery: ${mastery?.masteryLevel ?? "unassessed"}`,
524
+ `Accuracy: ${mastery?.accuracyPercent ?? 0}%`,
525
+ `Attempts: ${mastery?.totalAttempts ?? 0}`,
526
+ `Handout Viewed: ${handoutViewed ? "Yes" : "No"}`,
527
+ ""
528
+ ];
529
+ if (handout) {
530
+ lines.push("--- Concept Lesson ---", "", handout);
531
+ }
532
+ return { content: [{ type: "text", text: lines.join("\n") }] };
533
+ }
534
+ );
535
+ }
536
+
537
+ // src/tools/get-practice-question.ts
538
+ import { z as z3 } from "zod";
539
+
540
+ // src/engine/question-selector.ts
541
+ function selectNextQuestion(allQuestions, overdueReviews, weakAreas, answeredQuestionIds) {
542
+ if (overdueReviews.length > 0) {
543
+ const weakestFirst = [...overdueReviews].sort((a, b) => a.easeFactor - b.easeFactor);
544
+ for (const review of weakestFirst) {
545
+ const question = findUnansweredForTaskStatement(allQuestions, review.taskStatement, answeredQuestionIds);
546
+ if (question) return question;
547
+ }
548
+ }
549
+ if (weakAreas.length > 0) {
550
+ for (const area of weakAreas) {
551
+ const question = findUnansweredForTaskStatement(allQuestions, area.taskStatement, answeredQuestionIds);
552
+ if (question) return question;
553
+ }
554
+ }
555
+ return allQuestions.find((q) => !answeredQuestionIds.has(q.id));
556
+ }
557
+ function findUnansweredForTaskStatement(questions, taskStatement, answeredIds) {
558
+ return questions.find((q) => q.taskStatement === taskStatement && !answeredIds.has(q.id));
559
+ }
560
+
561
+ // src/tools/get-practice-question.ts
562
+ function registerGetPracticeQuestion(server2, db2, userConfig2) {
563
+ server2.tool(
564
+ "get_practice_question",
565
+ "Get the next practice question based on your learning progress. Prioritizes review questions, then weak areas, then new material.",
566
+ {
567
+ domainId: z3.number().optional().describe("Optional domain ID to filter questions (1-5)"),
568
+ difficulty: z3.enum(["easy", "medium", "hard"]).optional().describe("Optional difficulty filter")
569
+ },
570
+ async ({ domainId, difficulty }) => {
571
+ const userId = userConfig2.userId;
572
+ ensureUser(db2, userId);
573
+ const answeredIds = getAnsweredQuestionIds(db2, userId);
574
+ const overdueReviews = getOverdueReviews(db2, userId);
575
+ const weakAreas = getWeakAreas(db2, userId);
576
+ let questions = loadQuestions(domainId);
577
+ if (difficulty) {
578
+ questions = questions.filter((q) => q.difficulty === difficulty);
579
+ }
580
+ const question = selectNextQuestion(questions, overdueReviews, weakAreas, answeredIds);
581
+ if (!question) {
582
+ return {
583
+ content: [{ type: "text", text: "No more questions available for the selected criteria. Try a different domain or difficulty." }]
584
+ };
585
+ }
586
+ const response = {
587
+ questionId: question.id,
588
+ taskStatement: question.taskStatement,
589
+ domainId: question.domainId,
590
+ difficulty: question.difficulty,
591
+ scenario: question.scenario,
592
+ text: question.text,
593
+ options: question.options
594
+ };
595
+ return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
596
+ }
597
+ );
598
+ }
599
+
600
+ // src/tools/start-assessment.ts
601
+ function registerStartAssessment(server2, db2, userConfig2) {
602
+ server2.tool(
603
+ "start_assessment",
604
+ "Start the initial assessment with 15 questions (3 per domain: 1 easy, 1 medium, 1 hard) to determine your learning path.",
605
+ {},
606
+ async () => {
607
+ const userId = userConfig2.userId;
608
+ ensureUser(db2, userId);
609
+ const assessmentQuestions = [];
610
+ for (let d = 1; d <= 5; d++) {
611
+ const domainQuestions = loadQuestions(d);
612
+ const easy = domainQuestions.find((q) => q.difficulty === "easy");
613
+ const medium = domainQuestions.find((q) => q.difficulty === "medium");
614
+ const hard = domainQuestions.find((q) => q.difficulty === "hard");
615
+ if (easy) assessmentQuestions.push(easy);
616
+ if (medium) assessmentQuestions.push(medium);
617
+ if (hard) assessmentQuestions.push(hard);
618
+ }
619
+ if (assessmentQuestions.length === 0) {
620
+ return {
621
+ content: [{ type: "text", text: "No assessment questions available yet. The question bank is being populated." }]
622
+ };
623
+ }
624
+ const response = {
625
+ totalQuestions: assessmentQuestions.length,
626
+ questions: assessmentQuestions.map((q, i) => ({
627
+ number: i + 1,
628
+ questionId: q.id,
629
+ domainId: q.domainId,
630
+ difficulty: q.difficulty,
631
+ scenario: q.scenario,
632
+ text: q.text,
633
+ options: q.options
634
+ }))
635
+ };
636
+ return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
637
+ }
638
+ );
639
+ }
640
+
641
+ // src/tools/get-weak-areas.ts
642
+ function registerGetWeakAreas(server2, db2, userConfig2) {
643
+ server2.tool(
644
+ "get_weak_areas",
645
+ "Identify your weakest task statements based on accuracy below 70%. Focus your study on these areas.",
646
+ {},
647
+ async () => {
648
+ const userId = userConfig2.userId;
649
+ ensureUser(db2, userId);
650
+ const curriculum = loadCurriculum();
651
+ const weakAreas = getWeakAreas(db2, userId);
652
+ if (weakAreas.length === 0) {
653
+ return {
654
+ content: [{ type: "text", text: "No weak areas identified yet. Complete some questions first or all areas are above 70%!" }]
655
+ };
656
+ }
657
+ const lines = ["\u2550\u2550\u2550 WEAK AREAS \u2550\u2550\u2550", ""];
658
+ for (const area of weakAreas) {
659
+ const domain = curriculum.domains.find((d) => d.id === area.domainId);
660
+ const ts = domain?.taskStatements.find((t) => t.id === area.taskStatement);
661
+ lines.push(` ${area.taskStatement}: ${ts?.title ?? "Unknown"}`);
662
+ lines.push(` Accuracy: ${area.accuracyPercent}% (${area.correctAttempts}/${area.totalAttempts})`);
663
+ lines.push(` Mastery: ${area.masteryLevel}`);
664
+ lines.push("");
665
+ }
666
+ return { content: [{ type: "text", text: lines.join("\n") }] };
667
+ }
668
+ );
669
+ }
670
+
671
+ // src/engine/adaptive-path.ts
672
+ var BEGINNER_ORDER = [3, 4, 2, 1, 5];
673
+ var EXAM_WEIGHTED_ORDER = [1, 3, 4, 2, 5];
674
+ function getDomainOrder(path6) {
675
+ return path6 === "beginner-friendly" ? BEGINNER_ORDER : EXAM_WEIGHTED_ORDER;
676
+ }
677
+ function getNextRecommendedDomain(path6, masteryByDomain) {
678
+ const order = getDomainOrder(path6);
679
+ for (const domainId of order) {
680
+ const masteries = masteryByDomain.get(domainId) ?? [];
681
+ const avgAccuracy = masteries.length > 0 ? masteries.reduce((sum, m) => sum + m.accuracyPercent, 0) / masteries.length : 0;
682
+ if (avgAccuracy < 70) return domainId;
683
+ }
684
+ let weakestDomain = order[0];
685
+ let lowestAccuracy = 100;
686
+ for (const domainId of order) {
687
+ const masteries = masteryByDomain.get(domainId) ?? [];
688
+ const avg = masteries.length > 0 ? masteries.reduce((sum, m) => sum + m.accuracyPercent, 0) / masteries.length : 0;
689
+ if (avg < lowestAccuracy) {
690
+ lowestAccuracy = avg;
691
+ weakestDomain = domainId;
692
+ }
693
+ }
694
+ return weakestDomain;
695
+ }
696
+ function estimateTimeRemaining(totalQuestions, answeredQuestions, avgSecondsPerQuestion = 45) {
697
+ const remaining = totalQuestions - answeredQuestions;
698
+ const totalMinutes = Math.round(remaining * avgSecondsPerQuestion / 60);
699
+ const hours = Math.floor(totalMinutes / 60);
700
+ const minutes = totalMinutes % 60;
701
+ if (hours === 0) return `${minutes} minutes`;
702
+ return `${hours} hours ${minutes} minutes`;
703
+ }
704
+
705
+ // src/tools/get-study-plan.ts
706
+ function registerGetStudyPlan(server2, db2, userConfig2) {
707
+ server2.tool(
708
+ "get_study_plan",
709
+ "Get a personalized study plan based on your assessment results, weak areas, and learning path.",
710
+ {},
711
+ async () => {
712
+ const userId = userConfig2.userId;
713
+ ensureUser(db2, userId);
714
+ const user = getUser(db2, userId);
715
+ const curriculum = loadCurriculum();
716
+ const mastery = getAllMastery(db2, userId);
717
+ const overdueReviews = getOverdueReviews(db2, userId);
718
+ const stats = getTotalStats(db2, userId);
719
+ const allQuestions = loadQuestions();
720
+ const path6 = user?.learningPath ?? "beginner-friendly";
721
+ const masteryByDomain = /* @__PURE__ */ new Map();
722
+ for (const m of mastery) {
723
+ const existing = masteryByDomain.get(m.domainId) ?? [];
724
+ masteryByDomain.set(m.domainId, [...existing, m]);
725
+ }
726
+ const nextDomain = getNextRecommendedDomain(path6, masteryByDomain);
727
+ const domainOrder = getDomainOrder(path6);
728
+ const timeEstimate = estimateTimeRemaining(allQuestions.length, stats.total);
729
+ const domain = curriculum.domains.find((d) => d.id === nextDomain);
730
+ const lines = [
731
+ "\u2550\u2550\u2550 YOUR STUDY PLAN \u2550\u2550\u2550",
732
+ "",
733
+ `Learning Path: ${path6}`,
734
+ `Estimated Time Remaining: ${timeEstimate}`,
735
+ "",
736
+ `Next Recommended Domain: D${nextDomain} \u2014 ${domain?.title ?? "Unknown"}`,
737
+ "",
738
+ "Domain Study Order:",
739
+ ...domainOrder.map((id, i) => {
740
+ const d = curriculum.domains.find((x) => x.id === id);
741
+ return ` ${i + 1}. D${id}: ${d?.title ?? "Unknown"}`;
742
+ }),
743
+ "",
744
+ `Reviews Due: ${overdueReviews.length}`,
745
+ overdueReviews.length > 0 ? "Start with your overdue reviews before new material." : ""
746
+ ];
747
+ return { content: [{ type: "text", text: lines.join("\n") }] };
748
+ }
749
+ );
750
+ }
751
+
752
+ // src/tools/scaffold-project.ts
753
+ import fs4 from "fs";
754
+ import path4 from "path";
755
+ import { fileURLToPath as fileURLToPath2 } from "url";
756
+ import { z as z4 } from "zod";
757
+ var __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
758
+ var PROJECTS_DIR = path4.resolve(__dirname2, "..", "..", "projects");
759
+ var PROJECTS = [
760
+ { id: "capstone", name: "Capstone \u2014 Multi-Agent Research System", domains: [1, 2, 3, 4, 5] },
761
+ { id: "d1-agentic", name: "D1 Mini \u2014 Agentic Loop", domains: [1] },
762
+ { id: "d2-tools", name: "D2 Mini \u2014 Tool Design", domains: [2] },
763
+ { id: "d3-config", name: "D3 Mini \u2014 Claude Code Config", domains: [3] },
764
+ { id: "d4-prompts", name: "D4 Mini \u2014 Prompt Engineering", domains: [4] },
765
+ { id: "d5-context", name: "D5 Mini \u2014 Context Management", domains: [5] }
766
+ ];
767
+ function listFilesRecursive(dir, prefix = "") {
768
+ if (!fs4.existsSync(dir)) return [];
769
+ const entries = fs4.readdirSync(dir, { withFileTypes: true });
770
+ return entries.flatMap((entry) => {
771
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
772
+ if (entry.isDirectory()) {
773
+ return listFilesRecursive(path4.join(dir, entry.name), relativePath);
774
+ }
775
+ return [relativePath];
776
+ });
777
+ }
778
+ function registerScaffoldProject(server2, _db, _userConfig) {
779
+ server2.tool(
780
+ "scaffold_project",
781
+ "Get instructions for a reference project to practice certification concepts hands-on.",
782
+ { projectId: z4.string().optional().describe('Project ID (e.g. "capstone", "d1-agentic"). Omit to see available projects.') },
783
+ async ({ projectId }) => {
784
+ if (!projectId) {
785
+ const lines = [
786
+ "\u2550\u2550\u2550 REFERENCE PROJECTS \u2550\u2550\u2550",
787
+ "",
788
+ ...PROJECTS.map((p) => ` ${p.id}: ${p.name} (Domains: ${p.domains.join(", ")})`)
789
+ ];
790
+ return { content: [{ type: "text", text: lines.join("\n") }] };
791
+ }
792
+ const project = PROJECTS.find((p) => p.id === projectId);
793
+ if (!project) {
794
+ return {
795
+ content: [{ type: "text", text: `Project "${projectId}" not found. Use scaffold_project without arguments to see available projects.` }],
796
+ isError: true
797
+ };
798
+ }
799
+ const projectDir = path4.join(PROJECTS_DIR, projectId);
800
+ if (!fs4.existsSync(projectDir)) {
801
+ return {
802
+ content: [{ type: "text", text: `Project directory for "${project.name}" not found. The project files may not be installed yet.` }],
803
+ isError: true
804
+ };
805
+ }
806
+ const readmePath = path4.join(projectDir, "README.md");
807
+ const readme = fs4.existsSync(readmePath) ? fs4.readFileSync(readmePath, "utf-8") : null;
808
+ const files = listFilesRecursive(projectDir);
809
+ const sections = [
810
+ `\u2550\u2550\u2550 ${project.name} \u2550\u2550\u2550`,
811
+ "",
812
+ `Domains: ${project.domains.join(", ")}`,
813
+ ""
814
+ ];
815
+ if (readme) {
816
+ sections.push("--- README ---", "", readme, "");
817
+ }
818
+ sections.push(
819
+ "--- Project Files ---",
820
+ "",
821
+ ...files.map((f) => ` ${f}`),
822
+ "",
823
+ "--- Next Steps ---",
824
+ "",
825
+ "Explore the project files above to understand the architecture.",
826
+ "Each file demonstrates certification concepts in practice.",
827
+ `Project root: projects/${projectId}/`
828
+ );
829
+ return {
830
+ content: [{ type: "text", text: sections.join("\n") }]
831
+ };
832
+ }
833
+ );
834
+ }
835
+
836
+ // src/tools/reset-progress.ts
837
+ import { z as z5 } from "zod";
838
+ function registerResetProgress(server2, db2, userConfig2) {
839
+ server2.tool(
840
+ "reset_progress",
841
+ "WARNING: Permanently deletes ALL your study progress including answers, mastery data, and review schedules. This cannot be undone.",
842
+ { confirmed: z5.boolean().describe("Must be true to confirm the reset") },
843
+ async ({ confirmed }) => {
844
+ if (!confirmed) {
845
+ return { content: [{ type: "text", text: "Reset cancelled. Your progress is safe." }] };
846
+ }
847
+ const userId = userConfig2.userId;
848
+ db2.prepare("DELETE FROM answers WHERE userId = ?").run(userId);
849
+ db2.prepare("DELETE FROM domain_mastery WHERE userId = ?").run(userId);
850
+ db2.prepare("DELETE FROM review_schedule WHERE userId = ?").run(userId);
851
+ db2.prepare("DELETE FROM session_state WHERE userId = ?").run(userId);
852
+ db2.prepare("DELETE FROM study_sessions WHERE userId = ?").run(userId);
853
+ db2.prepare("DELETE FROM handout_views WHERE userId = ?").run(userId);
854
+ db2.prepare("DELETE FROM exam_attempts WHERE userId = ?").run(userId);
855
+ return { content: [{ type: "text", text: "All progress has been reset, including exam history. You can start fresh with start_assessment." }] };
856
+ }
857
+ );
858
+ }
859
+
860
+ // src/engine/exam-builder.ts
861
+ var EXAM_DISTRIBUTION = [
862
+ { domainId: 1, count: 16 },
863
+ // 27%
864
+ { domainId: 2, count: 11 },
865
+ // 18%
866
+ { domainId: 3, count: 12 },
867
+ // 20%
868
+ { domainId: 4, count: 12 },
869
+ // 20%
870
+ { domainId: 5, count: 9 }
871
+ // 15%
872
+ ];
873
+ function buildPracticeExam(allQuestions, previouslyUsedIds = /* @__PURE__ */ new Set()) {
874
+ const selected = [];
875
+ for (const { domainId, count } of EXAM_DISTRIBUTION) {
876
+ const domainQuestions = allQuestions.filter((q) => q.domainId === domainId);
877
+ const fresh = domainQuestions.filter((q) => !previouslyUsedIds.has(q.id));
878
+ const pool = fresh.length >= count ? fresh : domainQuestions;
879
+ const easy = shuffleArray(pool.filter((q) => q.difficulty === "easy"));
880
+ const medium = shuffleArray(pool.filter((q) => q.difficulty === "medium"));
881
+ const hard = shuffleArray(pool.filter((q) => q.difficulty === "hard"));
882
+ const easyCount = Math.round(count * 0.3);
883
+ const hardCount = Math.round(count * 0.3);
884
+ const mediumCount = count - easyCount - hardCount;
885
+ const pick = [
886
+ ...easy.slice(0, easyCount),
887
+ ...medium.slice(0, mediumCount),
888
+ ...hard.slice(0, hardCount)
889
+ ];
890
+ if (pick.length < count) {
891
+ const remaining = shuffleArray(
892
+ pool.filter((q) => !pick.some((p) => p.id === q.id))
893
+ );
894
+ pick.push(...remaining.slice(0, count - pick.length));
895
+ }
896
+ selected.push(...pick.slice(0, count));
897
+ }
898
+ return shuffleArray(selected);
899
+ }
900
+ function buildInitialDomainScores(questions, domainTitles) {
901
+ const scores = {};
902
+ for (const { domainId, count } of EXAM_DISTRIBUTION) {
903
+ scores[`d${domainId}`] = {
904
+ domainId,
905
+ domainTitle: domainTitles.get(domainId) ?? `Domain ${domainId}`,
906
+ totalQuestions: count,
907
+ correctAnswers: 0,
908
+ accuracyPercent: 0,
909
+ weight: getWeight(domainId)
910
+ };
911
+ }
912
+ return scores;
913
+ }
914
+ function getWeight(domainId) {
915
+ const weights = { 1: 27, 2: 18, 3: 20, 4: 20, 5: 15 };
916
+ return weights[domainId] ?? 0;
917
+ }
918
+ function shuffleArray(arr) {
919
+ const shuffled = [...arr];
920
+ for (let i = shuffled.length - 1; i > 0; i--) {
921
+ const j = Math.floor(Math.random() * (i + 1));
922
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
923
+ }
924
+ return shuffled;
925
+ }
926
+
927
+ // src/db/exam-attempts.ts
928
+ function createExamAttempt(db2, userId, questionIds) {
929
+ const stmt = db2.prepare(
930
+ "INSERT INTO exam_attempts (userId, totalQuestions, questionIds) VALUES (?, ?, ?)"
931
+ );
932
+ const result = stmt.run(userId, questionIds.length, JSON.stringify(questionIds));
933
+ return Number(result.lastInsertRowid);
934
+ }
935
+ function getActiveExam(db2, userId) {
936
+ const row = db2.prepare(
937
+ "SELECT * FROM exam_attempts WHERE userId = ? AND completedAt IS NULL ORDER BY startedAt DESC LIMIT 1"
938
+ ).get(userId);
939
+ return row ? rowToExamAttempt(row) : null;
940
+ }
941
+ function getExamById(db2, examId) {
942
+ const row = db2.prepare("SELECT * FROM exam_attempts WHERE id = ?").get(examId);
943
+ return row ? rowToExamAttempt(row) : null;
944
+ }
945
+ function recordExamAnswer(db2, examId, questionId, isCorrect, domainId) {
946
+ const row = db2.prepare("SELECT * FROM exam_attempts WHERE id = ?").get(examId);
947
+ if (!row) return;
948
+ const answeredIds = JSON.parse(row.answeredQuestionIds);
949
+ const updatedAnswered = [...answeredIds, questionId];
950
+ const newCorrect = row.correctAnswers + (isCorrect ? 1 : 0);
951
+ const domainScores = JSON.parse(row.domainScores);
952
+ const domainKey = `d${domainId}`;
953
+ const existing = domainScores[domainKey];
954
+ if (existing) {
955
+ const updatedCorrect = existing.correctAnswers + (isCorrect ? 1 : 0);
956
+ const updatedTotal = existing.totalQuestions;
957
+ domainScores[domainKey] = {
958
+ ...existing,
959
+ correctAnswers: updatedCorrect,
960
+ accuracyPercent: Math.round(updatedCorrect / updatedTotal * 100)
961
+ };
962
+ }
963
+ db2.prepare(
964
+ "UPDATE exam_attempts SET correctAnswers = ?, answeredQuestionIds = ?, domainScores = ? WHERE id = ?"
965
+ ).run(newCorrect, JSON.stringify(updatedAnswered), JSON.stringify(domainScores), examId);
966
+ }
967
+ function completeExam(db2, examId) {
968
+ const row = db2.prepare("SELECT * FROM exam_attempts WHERE id = ?").get(examId);
969
+ if (!row) return null;
970
+ const totalQuestions = row.totalQuestions;
971
+ const correctAnswers = row.correctAnswers;
972
+ const score = Math.round(correctAnswers / totalQuestions * 1e3);
973
+ const passed = score >= 720;
974
+ db2.prepare(
975
+ "UPDATE exam_attempts SET completedAt = CURRENT_TIMESTAMP, score = ?, passed = ? WHERE id = ?"
976
+ ).run(score, passed ? 1 : 0, examId);
977
+ return getExamById(db2, examId);
978
+ }
979
+ function getExamHistory(db2, userId) {
980
+ const rows = db2.prepare(
981
+ "SELECT * FROM exam_attempts WHERE userId = ? AND completedAt IS NOT NULL ORDER BY completedAt DESC"
982
+ ).all(userId);
983
+ return rows.map(rowToExamAttempt);
984
+ }
985
+ function rowToExamAttempt(row) {
986
+ return {
987
+ id: row.id,
988
+ userId: row.userId,
989
+ startedAt: row.startedAt,
990
+ completedAt: row.completedAt ?? null,
991
+ totalQuestions: row.totalQuestions,
992
+ correctAnswers: row.correctAnswers,
993
+ score: row.score,
994
+ passed: Boolean(row.passed),
995
+ questionIds: JSON.parse(row.questionIds),
996
+ answeredQuestionIds: JSON.parse(row.answeredQuestionIds),
997
+ domainScores: JSON.parse(row.domainScores)
998
+ };
999
+ }
1000
+
1001
+ // src/tools/start-practice-exam.ts
1002
+ function registerStartPracticeExam(server2, db2, userConfig2) {
1003
+ server2.tool(
1004
+ "start_practice_exam",
1005
+ "Start a full 60-question practice exam simulating the real Claude Certified Architect \u2014 Foundations exam. Questions are weighted by domain (D1: 16, D2: 11, D3: 12, D4: 12, D5: 9). Scored out of 1000, passing is 720. Results are saved for comparison across attempts.",
1006
+ {},
1007
+ async () => {
1008
+ const userId = userConfig2.userId;
1009
+ ensureUser(db2, userId);
1010
+ const active = getActiveExam(db2, userId);
1011
+ if (active) {
1012
+ const remaining = active.totalQuestions - active.answeredQuestionIds.length;
1013
+ return {
1014
+ content: [{
1015
+ type: "text",
1016
+ text: [
1017
+ "\u2550\u2550\u2550 PRACTICE EXAM IN PROGRESS \u2550\u2550\u2550",
1018
+ "",
1019
+ `You have an active practice exam (started ${active.startedAt}).`,
1020
+ `Progress: ${active.answeredQuestionIds.length}/${active.totalQuestions} questions answered`,
1021
+ `Remaining: ${remaining} questions`,
1022
+ "",
1023
+ "Use get_practice_exam_question to continue, or ask to abandon this exam first."
1024
+ ].join("\n")
1025
+ }]
1026
+ };
1027
+ }
1028
+ const allQuestions = loadQuestions();
1029
+ const curriculum = loadCurriculum();
1030
+ const domainTitles = new Map(curriculum.domains.map((d) => [d.id, d.title]));
1031
+ const history = getExamHistory(db2, userId);
1032
+ const recentIds = new Set(
1033
+ history.length > 0 ? history[0].questionIds : []
1034
+ );
1035
+ const examQuestions = buildPracticeExam(allQuestions, recentIds);
1036
+ const questionIds = examQuestions.map((q) => q.id);
1037
+ const domainScores = buildInitialDomainScores(examQuestions, domainTitles);
1038
+ const examId = createExamAttempt(db2, userId, questionIds);
1039
+ db2.prepare("UPDATE exam_attempts SET domainScores = ? WHERE id = ?").run(JSON.stringify(domainScores), examId);
1040
+ const distribution = EXAM_DISTRIBUTION.map(({ domainId, count }) => {
1041
+ const title = domainTitles.get(domainId) ?? `Domain ${domainId}`;
1042
+ return ` D${domainId}: ${title} \u2014 ${count} questions (${domainScores[`d${domainId}`].weight}%)`;
1043
+ });
1044
+ const firstQuestion = examQuestions[0];
1045
+ return {
1046
+ content: [{
1047
+ type: "text",
1048
+ text: [
1049
+ "\u2550\u2550\u2550 PRACTICE EXAM STARTED \u2550\u2550\u2550",
1050
+ "",
1051
+ "Simulating the Claude Certified Architect \u2014 Foundations exam.",
1052
+ "",
1053
+ `Exam ID: ${examId}`,
1054
+ "Total Questions: 60",
1055
+ "Passing Score: 720/1000",
1056
+ "",
1057
+ "Question Distribution:",
1058
+ ...distribution,
1059
+ "",
1060
+ "\u2500\u2500\u2500 Question 1 of 60 \u2500\u2500\u2500",
1061
+ "",
1062
+ `Domain: D${firstQuestion.domainId}`,
1063
+ `Task: ${firstQuestion.taskStatement}`,
1064
+ `Difficulty: ${firstQuestion.difficulty}`,
1065
+ "",
1066
+ `Scenario: ${firstQuestion.scenario}`,
1067
+ "",
1068
+ firstQuestion.text,
1069
+ "",
1070
+ `A) ${firstQuestion.options.A}`,
1071
+ `B) ${firstQuestion.options.B}`,
1072
+ `C) ${firstQuestion.options.C}`,
1073
+ `D) ${firstQuestion.options.D}`,
1074
+ "",
1075
+ `[Submit your answer using submit_exam_answer with examId: ${examId} and questionId: "${firstQuestion.id}"]`
1076
+ ].join("\n")
1077
+ }]
1078
+ };
1079
+ }
1080
+ );
1081
+ }
1082
+
1083
+ // src/tools/submit-exam-answer.ts
1084
+ import { z as z6 } from "zod";
1085
+ function registerSubmitExamAnswer(server2, db2, userConfig2) {
1086
+ server2.tool(
1087
+ "submit_exam_answer",
1088
+ "Submit an answer for a practice exam question. The answer is graded deterministically. After all 60 questions, the exam is scored and saved. DO NOT soften results \u2014 relay the grading output verbatim.",
1089
+ {
1090
+ examId: z6.number().describe("The practice exam ID"),
1091
+ questionId: z6.string().describe("The question ID being answered"),
1092
+ answer: z6.string().describe("Your answer: A, B, C, or D")
1093
+ },
1094
+ async ({ examId, questionId, answer }) => {
1095
+ const userId = userConfig2.userId;
1096
+ ensureUser(db2, userId);
1097
+ const exam = getExamById(db2, examId);
1098
+ if (!exam) {
1099
+ return {
1100
+ content: [{ type: "text", text: JSON.stringify({ error: "Exam not found", examId }) }],
1101
+ isError: true
1102
+ };
1103
+ }
1104
+ if (exam.completedAt) {
1105
+ return {
1106
+ content: [{ type: "text", text: "This exam is already completed. Start a new practice exam to try again." }],
1107
+ isError: true
1108
+ };
1109
+ }
1110
+ if (exam.answeredQuestionIds.includes(questionId)) {
1111
+ return {
1112
+ content: [{ type: "text", text: `Question ${questionId} has already been answered in this exam.` }],
1113
+ isError: true
1114
+ };
1115
+ }
1116
+ const allQuestions = loadQuestions();
1117
+ const question = allQuestions.find((q) => q.id === questionId);
1118
+ if (!question) {
1119
+ return {
1120
+ content: [{ type: "text", text: JSON.stringify({ error: "Question not found", questionId }) }],
1121
+ isError: true
1122
+ };
1123
+ }
1124
+ const result = gradeAnswer(question, answer);
1125
+ recordExamAnswer(db2, examId, questionId, result.isCorrect, question.domainId);
1126
+ const answeredCount = exam.answeredQuestionIds.length + 1;
1127
+ const remaining = exam.totalQuestions - answeredCount;
1128
+ const lines = [];
1129
+ if (result.isCorrect) {
1130
+ lines.push(`\u2705 Correct! (${answeredCount}/${exam.totalQuestions})`);
1131
+ } else {
1132
+ lines.push(`\u274C Incorrect. The correct answer is ${result.correctAnswer}. (${answeredCount}/${exam.totalQuestions})`);
1133
+ if (result.whyUserWasWrong) {
1134
+ lines.push("", `Why ${result.userAnswer} is wrong: ${result.whyUserWasWrong}`);
1135
+ }
1136
+ }
1137
+ lines.push("", result.explanation);
1138
+ if (remaining === 0) {
1139
+ const completed = completeExam(db2, examId);
1140
+ if (completed) {
1141
+ lines.push("", "\u2550\u2550\u2550 PRACTICE EXAM COMPLETE \u2550\u2550\u2550", "");
1142
+ lines.push(`Score: ${completed.score}/1000`);
1143
+ lines.push(`Result: ${completed.passed ? "\u2705 PASSED" : "\u274C FAILED"} (passing: 720/1000)`);
1144
+ lines.push(`Correct: ${completed.correctAnswers}/${completed.totalQuestions}`);
1145
+ lines.push("");
1146
+ lines.push("Domain Breakdown:");
1147
+ const scores = completed.domainScores;
1148
+ for (const key of Object.keys(scores).sort()) {
1149
+ const ds = scores[key];
1150
+ lines.push(` D${ds.domainId}: ${ds.domainTitle} \u2014 ${ds.correctAnswers}/${ds.totalQuestions} (${ds.accuracyPercent}%) [weight: ${ds.weight}%]`);
1151
+ }
1152
+ const history = getExamHistory(db2, userId);
1153
+ if (history.length > 1) {
1154
+ const previous = history[1];
1155
+ const scoreDiff = completed.score - previous.score;
1156
+ const arrow = scoreDiff > 0 ? "\u2191" : scoreDiff < 0 ? "\u2193" : "\u2192";
1157
+ lines.push("");
1158
+ lines.push("\u2500\u2500\u2500 Compared to Previous Attempt \u2500\u2500\u2500");
1159
+ lines.push(` Previous score: ${previous.score}/1000 ${previous.passed ? "(passed)" : "(failed)"}`);
1160
+ lines.push(` Change: ${arrow} ${scoreDiff > 0 ? "+" : ""}${scoreDiff} points`);
1161
+ for (const key of Object.keys(scores).sort()) {
1162
+ const current = scores[key];
1163
+ const prev = previous.domainScores[key];
1164
+ if (prev) {
1165
+ const diff = current.accuracyPercent - prev.accuracyPercent;
1166
+ const dArrow = diff > 0 ? "\u2191" : diff < 0 ? "\u2193" : "\u2192";
1167
+ lines.push(` D${current.domainId}: ${prev.accuracyPercent}% \u2192 ${current.accuracyPercent}% ${dArrow}`);
1168
+ }
1169
+ }
1170
+ }
1171
+ }
1172
+ } else {
1173
+ const nextQuestionId = exam.questionIds.find(
1174
+ (id) => !exam.answeredQuestionIds.includes(id) && id !== questionId
1175
+ );
1176
+ if (nextQuestionId) {
1177
+ const nextQuestion = allQuestions.find((q) => q.id === nextQuestionId);
1178
+ if (nextQuestion) {
1179
+ lines.push("");
1180
+ lines.push(`\u2500\u2500\u2500 Question ${answeredCount + 1} of ${exam.totalQuestions} \u2500\u2500\u2500`);
1181
+ lines.push("");
1182
+ lines.push(`Domain: D${nextQuestion.domainId}`);
1183
+ lines.push(`Task: ${nextQuestion.taskStatement}`);
1184
+ lines.push(`Difficulty: ${nextQuestion.difficulty}`);
1185
+ lines.push("");
1186
+ lines.push(`Scenario: ${nextQuestion.scenario}`);
1187
+ lines.push("");
1188
+ lines.push(nextQuestion.text);
1189
+ lines.push("");
1190
+ lines.push(`A) ${nextQuestion.options.A}`);
1191
+ lines.push(`B) ${nextQuestion.options.B}`);
1192
+ lines.push(`C) ${nextQuestion.options.C}`);
1193
+ lines.push(`D) ${nextQuestion.options.D}`);
1194
+ }
1195
+ }
1196
+ }
1197
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1198
+ }
1199
+ );
1200
+ }
1201
+
1202
+ // src/tools/get-exam-history.ts
1203
+ function registerGetExamHistory(server2, db2, userConfig2) {
1204
+ server2.tool(
1205
+ "get_exam_history",
1206
+ "View all completed practice exam attempts with scores, pass/fail status, and per-domain breakdowns. Compare your progress across attempts.",
1207
+ {},
1208
+ async () => {
1209
+ const userId = userConfig2.userId;
1210
+ ensureUser(db2, userId);
1211
+ const history = getExamHistory(db2, userId);
1212
+ if (history.length === 0) {
1213
+ return {
1214
+ content: [{
1215
+ type: "text",
1216
+ text: [
1217
+ "\u2550\u2550\u2550 EXAM HISTORY \u2550\u2550\u2550",
1218
+ "",
1219
+ "No completed practice exams yet.",
1220
+ "",
1221
+ "Use start_practice_exam to take your first 60-question practice exam.",
1222
+ "Questions are weighted by domain \u2014 just like the real exam."
1223
+ ].join("\n")
1224
+ }]
1225
+ };
1226
+ }
1227
+ const lines = [
1228
+ "\u2550\u2550\u2550 EXAM HISTORY \u2550\u2550\u2550",
1229
+ "",
1230
+ `Total Attempts: ${history.length}`,
1231
+ `Best Score: ${Math.max(...history.map((h) => h.score))}/1000`,
1232
+ `Latest Score: ${history[0].score}/1000`,
1233
+ ""
1234
+ ];
1235
+ for (const [i, attempt] of history.entries()) {
1236
+ const label = i === 0 ? " (latest)" : "";
1237
+ lines.push(`\u2500\u2500\u2500 Attempt #${history.length - i}${label} \u2500\u2500\u2500`);
1238
+ lines.push(` Date: ${attempt.completedAt ?? attempt.startedAt}`);
1239
+ lines.push(` Score: ${attempt.score}/1000 ${attempt.passed ? "\u2705 PASSED" : "\u274C FAILED"}`);
1240
+ lines.push(` Correct: ${attempt.correctAnswers}/${attempt.totalQuestions} (${Math.round(attempt.correctAnswers / attempt.totalQuestions * 100)}%)`);
1241
+ lines.push("");
1242
+ lines.push(" Domain Scores:");
1243
+ const scores = attempt.domainScores;
1244
+ for (const key of Object.keys(scores).sort()) {
1245
+ const ds = scores[key];
1246
+ lines.push(` D${ds.domainId}: ${ds.domainTitle} \u2014 ${ds.correctAnswers}/${ds.totalQuestions} (${ds.accuracyPercent}%)`);
1247
+ }
1248
+ if (i < history.length - 1) {
1249
+ const previous = history[i + 1];
1250
+ const diff = attempt.score - previous.score;
1251
+ const arrow = diff > 0 ? "\u2191" : diff < 0 ? "\u2193" : "\u2192";
1252
+ lines.push("");
1253
+ lines.push(` Change from previous: ${arrow} ${diff > 0 ? "+" : ""}${diff} points`);
1254
+ }
1255
+ lines.push("");
1256
+ }
1257
+ if (history.length >= 2) {
1258
+ const latest = history[0].score;
1259
+ const first = history[history.length - 1].score;
1260
+ const totalImprovement = latest - first;
1261
+ lines.push("\u2500\u2500\u2500 Overall Trend \u2500\u2500\u2500");
1262
+ lines.push(` First attempt: ${first}/1000`);
1263
+ lines.push(` Latest attempt: ${latest}/1000`);
1264
+ lines.push(` Total improvement: ${totalImprovement > 0 ? "+" : ""}${totalImprovement} points`);
1265
+ }
1266
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1267
+ }
1268
+ );
1269
+ }
1270
+
1271
+ // src/tools/follow-up.ts
1272
+ import { z as z7 } from "zod";
1273
+ var FOLLOW_UP_ACTIONS = ["next", "code_example", "concept", "handout", "project", "why_wrong"];
1274
+ var DOMAIN_PROJECT_MAP = {
1275
+ 1: "d1-agentic",
1276
+ 2: "d2-tools",
1277
+ 3: "d3-config",
1278
+ 4: "d4-prompts",
1279
+ 5: "d5-context"
1280
+ };
1281
+ function extractSection(markdown, sectionName) {
1282
+ const pattern = new RegExp(`^## ${sectionName}\\b`, "m");
1283
+ const match = pattern.exec(markdown);
1284
+ if (!match) return null;
1285
+ const startIndex = match.index + match[0].length;
1286
+ const nextSectionMatch = /^## /m.exec(markdown.slice(startIndex));
1287
+ const endIndex = nextSectionMatch ? startIndex + nextSectionMatch.index : markdown.length;
1288
+ return markdown.slice(startIndex, endIndex).trim();
1289
+ }
1290
+ function findQuestion(questionId) {
1291
+ const allQuestions = loadQuestions();
1292
+ return allQuestions.find((q) => q.id === questionId) ?? null;
1293
+ }
1294
+ function registerFollowUp(server2, _db, _userConfig) {
1295
+ server2.tool(
1296
+ "follow_up",
1297
+ "Handle post-answer follow-up actions. Use after submit_answer to explore concepts, code examples, handouts, or reference projects.",
1298
+ {
1299
+ questionId: z7.string().describe("The question ID from the previous answer"),
1300
+ action: z7.enum(FOLLOW_UP_ACTIONS).describe("The follow-up action to take")
1301
+ },
1302
+ async ({ questionId, action }) => {
1303
+ const question = findQuestion(questionId);
1304
+ if (!question) {
1305
+ return {
1306
+ content: [{ type: "text", text: JSON.stringify({ error: "Question not found", questionId }) }],
1307
+ isError: true
1308
+ };
1309
+ }
1310
+ switch (action) {
1311
+ case "next": {
1312
+ return {
1313
+ content: [{
1314
+ type: "text",
1315
+ text: JSON.stringify({
1316
+ instruction: "Call get_practice_question to get the next question.",
1317
+ taskStatement: question.taskStatement,
1318
+ domainId: question.domainId
1319
+ }, null, 2)
1320
+ }]
1321
+ };
1322
+ }
1323
+ case "code_example": {
1324
+ const handout = loadHandout(question.taskStatement);
1325
+ if (!handout) {
1326
+ return {
1327
+ content: [{ type: "text", text: JSON.stringify({ error: "No handout found for this task statement", taskStatement: question.taskStatement }) }],
1328
+ isError: true
1329
+ };
1330
+ }
1331
+ const codeExample = extractSection(handout, "Code Example");
1332
+ if (!codeExample) {
1333
+ return {
1334
+ content: [{ type: "text", text: JSON.stringify({ error: "No Code Example section found in handout", taskStatement: question.taskStatement }) }],
1335
+ isError: true
1336
+ };
1337
+ }
1338
+ return {
1339
+ content: [{
1340
+ type: "text",
1341
+ text: JSON.stringify({
1342
+ taskStatement: question.taskStatement,
1343
+ codeExample
1344
+ }, null, 2)
1345
+ }]
1346
+ };
1347
+ }
1348
+ case "concept": {
1349
+ const handout = loadHandout(question.taskStatement);
1350
+ if (!handout) {
1351
+ return {
1352
+ content: [{ type: "text", text: JSON.stringify({ error: "No handout found for this task statement", taskStatement: question.taskStatement }) }],
1353
+ isError: true
1354
+ };
1355
+ }
1356
+ const concept = extractSection(handout, "Concept");
1357
+ if (!concept) {
1358
+ return {
1359
+ content: [{ type: "text", text: JSON.stringify({ error: "No Concept section found in handout", taskStatement: question.taskStatement }) }],
1360
+ isError: true
1361
+ };
1362
+ }
1363
+ return {
1364
+ content: [{
1365
+ type: "text",
1366
+ text: JSON.stringify({
1367
+ taskStatement: question.taskStatement,
1368
+ concept
1369
+ }, null, 2)
1370
+ }]
1371
+ };
1372
+ }
1373
+ case "handout": {
1374
+ const handout = loadHandout(question.taskStatement);
1375
+ if (!handout) {
1376
+ return {
1377
+ content: [{ type: "text", text: JSON.stringify({ error: "No handout found for this task statement", taskStatement: question.taskStatement }) }],
1378
+ isError: true
1379
+ };
1380
+ }
1381
+ return {
1382
+ content: [{
1383
+ type: "text",
1384
+ text: JSON.stringify({
1385
+ taskStatement: question.taskStatement,
1386
+ handout
1387
+ }, null, 2)
1388
+ }]
1389
+ };
1390
+ }
1391
+ case "project": {
1392
+ const projectId = DOMAIN_PROJECT_MAP[question.domainId] ?? null;
1393
+ if (!projectId) {
1394
+ return {
1395
+ content: [{ type: "text", text: JSON.stringify({ error: "No reference project mapped for this domain", domainId: question.domainId }) }],
1396
+ isError: true
1397
+ };
1398
+ }
1399
+ return {
1400
+ content: [{
1401
+ type: "text",
1402
+ text: JSON.stringify({
1403
+ instruction: "Call scaffold_project to explore the reference project for this domain.",
1404
+ projectId,
1405
+ domainId: question.domainId
1406
+ }, null, 2)
1407
+ }]
1408
+ };
1409
+ }
1410
+ case "why_wrong": {
1411
+ const incorrectOptions = Object.entries(question.whyWrongMap).filter(([key]) => key !== question.correctAnswer).reduce((acc, [key, value]) => {
1412
+ if (value) {
1413
+ return { ...acc, [key]: value };
1414
+ }
1415
+ return acc;
1416
+ }, {});
1417
+ return {
1418
+ content: [{
1419
+ type: "text",
1420
+ text: JSON.stringify({
1421
+ questionId: question.id,
1422
+ correctAnswer: question.correctAnswer,
1423
+ explanation: question.explanation,
1424
+ whyOthersAreWrong: incorrectOptions
1425
+ }, null, 2)
1426
+ }]
1427
+ };
1428
+ }
1429
+ }
1430
+ }
1431
+ );
1432
+ }
1433
+
1434
+ // src/tools/start-capstone-build.ts
1435
+ import { z as z8 } from "zod";
1436
+
1437
+ // src/data/criteria.ts
1438
+ var DOMAIN_NAMES = {
1439
+ 1: "Agentic Architecture & Orchestration",
1440
+ 2: "Tool Design & MCP Integration",
1441
+ 3: "Claude Code Configuration & Workflows",
1442
+ 4: "Prompt Engineering & Structured Output",
1443
+ 5: "Context Management & Reliability"
1444
+ };
1445
+ var CRITERIA = [
1446
+ // Domain 1: Agentic Architecture & Orchestration
1447
+ {
1448
+ id: "1.1",
1449
+ title: "Design and implement agentic loops for autonomous task execution",
1450
+ domain: 1,
1451
+ domainName: DOMAIN_NAMES[1],
1452
+ description: "Understanding the agentic loop lifecycle: sending requests, inspecting stop_reason, executing tools, and returning results."
1453
+ },
1454
+ {
1455
+ id: "1.2",
1456
+ title: "Orchestrate multi-agent systems with coordinator-subagent patterns",
1457
+ domain: 1,
1458
+ domainName: DOMAIN_NAMES[1],
1459
+ description: "Hub-and-spoke architecture, isolated context, task decomposition, and result aggregation."
1460
+ },
1461
+ {
1462
+ id: "1.3",
1463
+ title: "Configure subagent invocation, context passing, and spawning",
1464
+ domain: 1,
1465
+ domainName: DOMAIN_NAMES[1],
1466
+ description: "Task tool, allowedTools, explicit context passing, parallel subagent execution."
1467
+ },
1468
+ {
1469
+ id: "1.4",
1470
+ title: "Implement multi-step workflows with enforcement and handoff patterns",
1471
+ domain: 1,
1472
+ domainName: DOMAIN_NAMES[1],
1473
+ description: "Programmatic enforcement vs prompt-based guidance, structured handoff protocols."
1474
+ },
1475
+ {
1476
+ id: "1.5",
1477
+ title: "Apply Agent SDK hooks for tool call interception and data normalization",
1478
+ domain: 1,
1479
+ domainName: DOMAIN_NAMES[1],
1480
+ description: "PostToolUse hooks, tool call interception, deterministic vs probabilistic compliance."
1481
+ },
1482
+ {
1483
+ id: "1.6",
1484
+ title: "Design task decomposition strategies for complex workflows",
1485
+ domain: 1,
1486
+ domainName: DOMAIN_NAMES[1],
1487
+ description: "Prompt chaining vs dynamic decomposition, per-file analysis vs cross-file integration."
1488
+ },
1489
+ {
1490
+ id: "1.7",
1491
+ title: "Manage session state, resumption, and forking",
1492
+ domain: 1,
1493
+ domainName: DOMAIN_NAMES[1],
1494
+ description: "Named sessions, fork_session, structured summaries vs stale context."
1495
+ },
1496
+ // Domain 2: Tool Design & MCP Integration
1497
+ {
1498
+ id: "2.1",
1499
+ title: "Design effective tool interfaces with clear descriptions and boundaries",
1500
+ domain: 2,
1501
+ domainName: DOMAIN_NAMES[2],
1502
+ description: "Tool descriptions as selection mechanism, disambiguation, splitting vs consolidating."
1503
+ },
1504
+ {
1505
+ id: "2.2",
1506
+ title: "Implement structured error responses for MCP tools",
1507
+ domain: 2,
1508
+ domainName: DOMAIN_NAMES[2],
1509
+ description: "isError flag, error categories, retryable vs non-retryable, structured metadata."
1510
+ },
1511
+ {
1512
+ id: "2.3",
1513
+ title: "Distribute tools appropriately across agents and configure tool choice",
1514
+ domain: 2,
1515
+ domainName: DOMAIN_NAMES[2],
1516
+ description: "Scoped tool access, tool_choice options, forced selection patterns."
1517
+ },
1518
+ {
1519
+ id: "2.4",
1520
+ title: "Integrate MCP servers into Claude Code and agent workflows",
1521
+ domain: 2,
1522
+ domainName: DOMAIN_NAMES[2],
1523
+ description: "Project vs user scope, .mcp.json, environment variable expansion, MCP resources."
1524
+ },
1525
+ {
1526
+ id: "2.5",
1527
+ title: "Select and apply built-in tools effectively",
1528
+ domain: 2,
1529
+ domainName: DOMAIN_NAMES[2],
1530
+ description: "Grep vs Glob vs Read/Write/Edit, incremental codebase understanding."
1531
+ },
1532
+ // Domain 3: Claude Code Configuration & Workflows
1533
+ {
1534
+ id: "3.1",
1535
+ title: "Configure CLAUDE.md files with appropriate hierarchy and scoping",
1536
+ domain: 3,
1537
+ domainName: DOMAIN_NAMES[3],
1538
+ description: "User-level, project-level, directory-level, @import syntax, .claude/rules/."
1539
+ },
1540
+ {
1541
+ id: "3.2",
1542
+ title: "Create and configure custom slash commands and skills",
1543
+ domain: 3,
1544
+ domainName: DOMAIN_NAMES[3],
1545
+ description: "Project vs user scope, context: fork, allowed-tools, argument-hint frontmatter."
1546
+ },
1547
+ {
1548
+ id: "3.3",
1549
+ title: "Apply path-specific rules for conditional convention loading",
1550
+ domain: 3,
1551
+ domainName: DOMAIN_NAMES[3],
1552
+ description: "YAML frontmatter paths, glob patterns, conditional activation."
1553
+ },
1554
+ {
1555
+ id: "3.4",
1556
+ title: "Determine when to use plan mode vs direct execution",
1557
+ domain: 3,
1558
+ domainName: DOMAIN_NAMES[3],
1559
+ description: "Complexity assessment, architectural decisions, Explore subagent."
1560
+ },
1561
+ {
1562
+ id: "3.5",
1563
+ title: "Apply iterative refinement techniques for progressive improvement",
1564
+ domain: 3,
1565
+ domainName: DOMAIN_NAMES[3],
1566
+ description: "Input/output examples, test-driven iteration, interview pattern."
1567
+ },
1568
+ {
1569
+ id: "3.6",
1570
+ title: "Integrate Claude Code into CI/CD pipelines",
1571
+ domain: 3,
1572
+ domainName: DOMAIN_NAMES[3],
1573
+ description: "-p flag, --output-format json, --json-schema, session context isolation."
1574
+ },
1575
+ // Domain 4: Prompt Engineering & Structured Output
1576
+ {
1577
+ id: "4.1",
1578
+ title: "Design prompts with explicit criteria to improve precision",
1579
+ domain: 4,
1580
+ domainName: DOMAIN_NAMES[4],
1581
+ description: "Explicit criteria vs vague instructions, false positive management."
1582
+ },
1583
+ {
1584
+ id: "4.2",
1585
+ title: "Apply few-shot prompting to improve output consistency",
1586
+ domain: 4,
1587
+ domainName: DOMAIN_NAMES[4],
1588
+ description: "Targeted examples, ambiguous case handling, format demonstration."
1589
+ },
1590
+ {
1591
+ id: "4.3",
1592
+ title: "Enforce structured output using tool use and JSON schemas",
1593
+ domain: 4,
1594
+ domainName: DOMAIN_NAMES[4],
1595
+ description: "tool_use with schemas, tool_choice options, nullable fields, enum patterns."
1596
+ },
1597
+ {
1598
+ id: "4.4",
1599
+ title: "Implement validation, retry, and feedback loops",
1600
+ domain: 4,
1601
+ domainName: DOMAIN_NAMES[4],
1602
+ description: "Retry-with-error-feedback, limits of retry, detected_pattern tracking."
1603
+ },
1604
+ {
1605
+ id: "4.5",
1606
+ title: "Design efficient batch processing strategies",
1607
+ domain: 4,
1608
+ domainName: DOMAIN_NAMES[4],
1609
+ description: "Message Batches API, latency tolerance, custom_id, failure handling."
1610
+ },
1611
+ {
1612
+ id: "4.6",
1613
+ title: "Design multi-instance and multi-pass review architectures",
1614
+ domain: 4,
1615
+ domainName: DOMAIN_NAMES[4],
1616
+ description: "Self-review limitations, independent review instances, per-file + cross-file passes."
1617
+ },
1618
+ // Domain 5: Context Management & Reliability
1619
+ {
1620
+ id: "5.1",
1621
+ title: "Manage conversation context to preserve critical information",
1622
+ domain: 5,
1623
+ domainName: DOMAIN_NAMES[5],
1624
+ description: "Progressive summarization risks, lost-in-the-middle, tool output trimming."
1625
+ },
1626
+ {
1627
+ id: "5.2",
1628
+ title: "Design effective escalation and ambiguity resolution patterns",
1629
+ domain: 5,
1630
+ domainName: DOMAIN_NAMES[5],
1631
+ description: "Escalation triggers, customer preferences, sentiment unreliability."
1632
+ },
1633
+ {
1634
+ id: "5.3",
1635
+ title: "Implement error propagation strategies across multi-agent systems",
1636
+ domain: 5,
1637
+ domainName: DOMAIN_NAMES[5],
1638
+ description: "Structured error context, access failures vs empty results, partial results."
1639
+ },
1640
+ {
1641
+ id: "5.4",
1642
+ title: "Manage context effectively in large codebase exploration",
1643
+ domain: 5,
1644
+ domainName: DOMAIN_NAMES[5],
1645
+ description: "Context degradation, scratchpad files, subagent delegation, /compact."
1646
+ },
1647
+ {
1648
+ id: "5.5",
1649
+ title: "Design human review workflows and confidence calibration",
1650
+ domain: 5,
1651
+ domainName: DOMAIN_NAMES[5],
1652
+ description: "Stratified sampling, field-level confidence, accuracy by document type."
1653
+ },
1654
+ {
1655
+ id: "5.6",
1656
+ title: "Preserve information provenance and handle uncertainty in synthesis",
1657
+ domain: 5,
1658
+ domainName: DOMAIN_NAMES[5],
1659
+ description: "Claim-source mappings, conflict annotation, temporal data handling."
1660
+ }
1661
+ ];
1662
+
1663
+ // src/db/capstone.ts
1664
+ import crypto2 from "crypto";
1665
+ function getActiveBuild(db2, userId) {
1666
+ const row = db2.prepare(
1667
+ `SELECT * FROM capstone_builds WHERE userId = ? AND status IN ('shaping', 'building') ORDER BY createdAt DESC LIMIT 1`
1668
+ ).get(userId);
1669
+ return row ?? null;
1670
+ }
1671
+ function createBuild(db2, userId, theme) {
1672
+ const id = crypto2.randomUUID();
1673
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1674
+ db2.prepare(
1675
+ `INSERT INTO capstone_builds (id, userId, theme, currentStep, status, themeValidated, createdAt, updatedAt)
1676
+ VALUES (?, ?, ?, 0, 'shaping', 0, ?, ?)`
1677
+ ).run(id, userId, theme, now, now);
1678
+ return db2.prepare("SELECT * FROM capstone_builds WHERE id = ?").get(id);
1679
+ }
1680
+ function updateBuildTheme(db2, buildId, theme) {
1681
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1682
+ db2.prepare(
1683
+ `UPDATE capstone_builds SET theme = ?, updatedAt = ? WHERE id = ?`
1684
+ ).run(theme, now, buildId);
1685
+ }
1686
+ function confirmBuild(db2, buildId) {
1687
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1688
+ db2.prepare(
1689
+ `UPDATE capstone_builds SET status = 'building', themeValidated = 1, currentStep = 1, updatedAt = ? WHERE id = ?`
1690
+ ).run(now, buildId);
1691
+ }
1692
+ function abandonBuild(db2, buildId) {
1693
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1694
+ db2.prepare(
1695
+ `UPDATE capstone_builds SET status = 'abandoned', updatedAt = ? WHERE id = ?`
1696
+ ).run(now, buildId);
1697
+ }
1698
+ function completeBuild(db2, buildId) {
1699
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1700
+ db2.prepare(
1701
+ `UPDATE capstone_builds SET status = 'completed', updatedAt = ? WHERE id = ?`
1702
+ ).run(now, buildId);
1703
+ }
1704
+ function getBuildStep(db2, buildId, stepIndex) {
1705
+ const row = db2.prepare(
1706
+ `SELECT * FROM capstone_build_steps WHERE buildId = ? AND stepIndex = ?`
1707
+ ).get(buildId, stepIndex);
1708
+ return row ?? null;
1709
+ }
1710
+ function createBuildSteps(db2, buildId, steps) {
1711
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1712
+ const insert = db2.prepare(
1713
+ `INSERT INTO capstone_build_steps (id, buildId, stepIndex, fileName, taskStatements, quizQuestionIds, quizCompleted, buildCompleted, walkthroughViewed, createdAt, updatedAt)
1714
+ VALUES (?, ?, ?, ?, ?, NULL, 0, 0, 0, ?, ?)`
1715
+ );
1716
+ const insertAll = db2.transaction((stepsToInsert) => {
1717
+ for (const step of stepsToInsert) {
1718
+ insert.run(
1719
+ crypto2.randomUUID(),
1720
+ buildId,
1721
+ step.stepIndex,
1722
+ step.fileName,
1723
+ JSON.stringify(step.taskStatements),
1724
+ now,
1725
+ now
1726
+ );
1727
+ }
1728
+ });
1729
+ insertAll(steps);
1730
+ }
1731
+ function updateBuildStep(db2, stepId, updates) {
1732
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1733
+ const setClauses = ["updatedAt = ?"];
1734
+ const params = [now];
1735
+ if (updates.quizCompleted !== void 0) {
1736
+ setClauses.push("quizCompleted = ?");
1737
+ params.push(updates.quizCompleted);
1738
+ }
1739
+ if (updates.buildCompleted !== void 0) {
1740
+ setClauses.push("buildCompleted = ?");
1741
+ params.push(updates.buildCompleted);
1742
+ }
1743
+ if (updates.walkthroughViewed !== void 0) {
1744
+ setClauses.push("walkthroughViewed = ?");
1745
+ params.push(updates.walkthroughViewed);
1746
+ }
1747
+ params.push(stepId);
1748
+ db2.prepare(`UPDATE capstone_build_steps SET ${setClauses.join(", ")} WHERE id = ?`).run(...params);
1749
+ }
1750
+ function setQuizQuestionIds(db2, stepId, questionIds) {
1751
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1752
+ db2.prepare(
1753
+ `UPDATE capstone_build_steps SET quizQuestionIds = ?, updatedAt = ? WHERE id = ?`
1754
+ ).run(JSON.stringify(questionIds), now, stepId);
1755
+ }
1756
+ function advanceBuildStep(db2, buildId, nextStep) {
1757
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1758
+ db2.prepare(
1759
+ `UPDATE capstone_builds SET currentStep = ?, updatedAt = ? WHERE id = ?`
1760
+ ).run(nextStep, now, buildId);
1761
+ }
1762
+ function getBuildSteps(db2, buildId) {
1763
+ return db2.prepare(
1764
+ `SELECT * FROM capstone_build_steps WHERE buildId = ? ORDER BY stepIndex ASC`
1765
+ ).all(buildId);
1766
+ }
1767
+
1768
+ // src/tools/start-capstone-build.ts
1769
+ function formatCriteria() {
1770
+ const lines = [];
1771
+ let currentDomain = 0;
1772
+ for (const criterion of CRITERIA) {
1773
+ if (criterion.domain !== currentDomain) {
1774
+ currentDomain = criterion.domain;
1775
+ if (lines.length > 0) lines.push("");
1776
+ lines.push(`Domain ${criterion.domain}: ${criterion.domainName}`);
1777
+ }
1778
+ lines.push(` ${criterion.id} \u2014 ${criterion.title}: ${criterion.description}`);
1779
+ }
1780
+ return lines.join("\n");
1781
+ }
1782
+ function buildResponse(theme) {
1783
+ const sections = [
1784
+ "=== GUIDED CAPSTONE BUILD ===",
1785
+ "",
1786
+ "--- 30 Architectural Criteria ---",
1787
+ "",
1788
+ formatCriteria()
1789
+ ];
1790
+ if (theme) {
1791
+ sections.push(
1792
+ "",
1793
+ "--- Your Project Theme ---",
1794
+ theme,
1795
+ "",
1796
+ "--- Instructions ---",
1797
+ "Review the criteria above against your project idea. Claude will analyze",
1798
+ "which criteria are naturally covered and suggest modifications for any gaps.",
1799
+ "When you're satisfied with coverage, use capstone_build_step with action",
1800
+ "'confirm' to begin building."
1801
+ );
1802
+ } else {
1803
+ sections.push(
1804
+ "",
1805
+ "--- Instructions ---",
1806
+ "Choose a project theme that excites you. The best capstone projects are ones",
1807
+ "you actually want to build. Provide your theme using start_capstone_build",
1808
+ "with a 'theme' parameter, and Claude will analyze how well it covers the",
1809
+ "30 criteria above."
1810
+ );
1811
+ }
1812
+ return sections.join("\n");
1813
+ }
1814
+ function registerStartCapstoneBuild(server2, db2, userConfig2) {
1815
+ server2.tool(
1816
+ "start_capstone_build",
1817
+ "Start or refine a guided capstone build. Build your own project while learning all 30 certification task statements hands-on.",
1818
+ {
1819
+ theme: z8.string().optional().describe("Your project idea or theme. Omit to see the 30 criteria first.")
1820
+ },
1821
+ async ({ theme }) => {
1822
+ const userId = userConfig2.userId;
1823
+ ensureUser(db2, userId);
1824
+ if (!theme) {
1825
+ return {
1826
+ content: [{ type: "text", text: buildResponse(null) }]
1827
+ };
1828
+ }
1829
+ const activeBuild = getActiveBuild(db2, userId);
1830
+ if (activeBuild && activeBuild.status === "building") {
1831
+ return {
1832
+ content: [{
1833
+ type: "text",
1834
+ text: "You have an active build in progress. Use capstone_build_step with action 'abandon' to start over."
1835
+ }],
1836
+ isError: true
1837
+ };
1838
+ }
1839
+ if (activeBuild && activeBuild.status === "shaping") {
1840
+ updateBuildTheme(db2, activeBuild.id, theme);
1841
+ return {
1842
+ content: [{ type: "text", text: buildResponse(theme) }]
1843
+ };
1844
+ }
1845
+ createBuild(db2, userId, theme);
1846
+ return {
1847
+ content: [{ type: "text", text: buildResponse(theme) }]
1848
+ };
1849
+ }
1850
+ );
1851
+ }
1852
+
1853
+ // src/tools/capstone-build-step.ts
1854
+ import { z as z9 } from "zod";
1855
+
1856
+ // src/data/build-steps.ts
1857
+ var BUILD_STEPS = [
1858
+ {
1859
+ stepIndex: 1,
1860
+ fileName: "CLAUDE.md, .claude/",
1861
+ taskStatements: ["3.1", "3.2", "3.3"],
1862
+ description: "Project config and rules",
1863
+ codeHints: "Generate a CLAUDE.md with hierarchical instructions, @import references, and .claude/rules/ with path-scoped YAML frontmatter. Include a custom slash command definition."
1864
+ },
1865
+ {
1866
+ stepIndex: 2,
1867
+ fileName: "package.json, tsconfig.json",
1868
+ taskStatements: ["3.4"],
1869
+ description: "Project setup and CI hooks",
1870
+ codeHints: "Set up TypeScript project configuration with strict compiler options, lint/test scripts suitable for plan-mode assessment, and pre-commit hooks."
1871
+ },
1872
+ {
1873
+ stepIndex: 3,
1874
+ fileName: "src/server.ts",
1875
+ taskStatements: ["2.1", "2.2"],
1876
+ description: "MCP server with tool registration",
1877
+ codeHints: "Create an MCP server entry point that registers tools with clear descriptions and boundaries, and returns structured error responses with isError flags."
1878
+ },
1879
+ {
1880
+ stepIndex: 4,
1881
+ fileName: "src/tools/",
1882
+ taskStatements: ["2.1", "2.3", "2.5"],
1883
+ description: "Tool definitions and scoping",
1884
+ codeHints: "Define tool modules with scoped access per agent, tool_choice configuration, and demonstrate effective use of built-in tools like Grep, Glob, and Read."
1885
+ },
1886
+ {
1887
+ stepIndex: 5,
1888
+ fileName: "src/error-handling.ts",
1889
+ taskStatements: ["2.2"],
1890
+ description: "Error boundaries and recovery",
1891
+ codeHints: "Implement error boundary utilities that categorize errors as retryable vs non-retryable, attach structured metadata, and format MCP-compliant error responses."
1892
+ },
1893
+ {
1894
+ stepIndex: 6,
1895
+ fileName: "src/coordinator.ts",
1896
+ taskStatements: ["1.1", "1.2", "1.6"],
1897
+ description: "Main agentic loop",
1898
+ codeHints: "Build a coordinator that runs the agentic loop (send request, inspect stop_reason, execute tools, return results) with hub-and-spoke orchestration and task decomposition."
1899
+ },
1900
+ {
1901
+ stepIndex: 7,
1902
+ fileName: "src/subagents/",
1903
+ taskStatements: ["1.3", "1.4"],
1904
+ description: "Subagent definitions and routing",
1905
+ codeHints: "Define subagent configurations with allowedTools, explicit context passing, and structured handoff protocols for multi-step workflow enforcement."
1906
+ },
1907
+ {
1908
+ stepIndex: 8,
1909
+ fileName: "src/hooks.ts",
1910
+ taskStatements: ["1.5"],
1911
+ description: "Pre/post tool-use hooks",
1912
+ codeHints: "Implement PostToolUse hooks that intercept tool calls for data normalization, demonstrating deterministic compliance checks vs probabilistic validation."
1913
+ },
1914
+ {
1915
+ stepIndex: 9,
1916
+ fileName: "src/workflow.ts",
1917
+ taskStatements: ["1.4", "1.6"],
1918
+ description: "Multi-step workflows",
1919
+ codeHints: "Create workflow orchestration with programmatic enforcement gates, prompt chaining stages, and per-file analysis that feeds into cross-file integration."
1920
+ },
1921
+ {
1922
+ stepIndex: 10,
1923
+ fileName: "src/session.ts",
1924
+ taskStatements: ["1.7"],
1925
+ description: "Session and state management",
1926
+ codeHints: "Implement session lifecycle with named sessions, fork_session for parallel exploration, and structured summaries to avoid stale context on resumption."
1927
+ },
1928
+ {
1929
+ stepIndex: 11,
1930
+ fileName: "src/prompts/system.ts",
1931
+ taskStatements: ["4.1", "4.2"],
1932
+ description: "System prompts with few-shot",
1933
+ codeHints: "Design system prompts with explicit criteria for precision, and embed few-shot examples that handle ambiguous cases and demonstrate expected output format."
1934
+ },
1935
+ {
1936
+ stepIndex: 12,
1937
+ fileName: "src/prompts/extraction.ts",
1938
+ taskStatements: ["4.3", "4.4"],
1939
+ description: "Structured output and validation",
1940
+ codeHints: "Enforce structured output via tool_use with JSON schemas and enum patterns, plus retry-with-error-feedback loops that track detected_pattern for progressive improvement."
1941
+ },
1942
+ {
1943
+ stepIndex: 13,
1944
+ fileName: "src/prompts/batch.ts",
1945
+ taskStatements: ["4.5", "4.6"],
1946
+ description: "Batch processing and multi-pass",
1947
+ codeHints: "Implement batch processing using the Message Batches API with custom_id tracking and failure handling, plus multi-pass review with independent instances."
1948
+ },
1949
+ {
1950
+ stepIndex: 14,
1951
+ fileName: "src/context/preservation.ts",
1952
+ taskStatements: ["5.1"],
1953
+ description: "Context preservation strategies",
1954
+ codeHints: "Implement context preservation that mitigates progressive summarization risks and lost-in-the-middle effects, with tool output trimming to retain critical information."
1955
+ },
1956
+ {
1957
+ stepIndex: 15,
1958
+ fileName: "src/context/triggers.ts",
1959
+ taskStatements: ["5.2"],
1960
+ description: "Context refresh triggers",
1961
+ codeHints: "Define escalation triggers and ambiguity resolution patterns that detect when context needs refreshing, using customer preference signals rather than unreliable sentiment."
1962
+ },
1963
+ {
1964
+ stepIndex: 16,
1965
+ fileName: "src/context/propagation.ts",
1966
+ taskStatements: ["5.3"],
1967
+ description: "Cross-agent context propagation",
1968
+ codeHints: "Implement structured error context propagation across agents, distinguishing access failures from empty results and handling partial result aggregation."
1969
+ },
1970
+ {
1971
+ stepIndex: 17,
1972
+ fileName: "src/context/scratchpad.ts",
1973
+ taskStatements: ["5.4"],
1974
+ description: "Scratchpad and subagent delegation",
1975
+ codeHints: "Build scratchpad file management for large codebase exploration, with subagent delegation to prevent context degradation and /compact integration."
1976
+ },
1977
+ {
1978
+ stepIndex: 18,
1979
+ fileName: "src/context/confidence.ts",
1980
+ taskStatements: ["5.5", "5.6"],
1981
+ description: "Confidence calibration and synthesis",
1982
+ codeHints: "Implement field-level confidence scoring with stratified sampling for human review, plus claim-source mappings with conflict annotation for provenance tracking."
1983
+ }
1984
+ ];
1985
+
1986
+ // src/tools/capstone-build-step.ts
1987
+ var ACTIONS = ["confirm", "quiz", "build", "next", "status", "abandon"];
1988
+ var TOTAL_STEPS = 18;
1989
+ function errorResponse(message) {
1990
+ return {
1991
+ content: [{ type: "text", text: JSON.stringify({ error: message }) }],
1992
+ isError: true
1993
+ };
1994
+ }
1995
+ function textResponse(data) {
1996
+ return {
1997
+ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }]
1998
+ };
1999
+ }
2000
+ function getStepTemplate(stepIndex) {
2001
+ return BUILD_STEPS.find((s) => s.stepIndex === stepIndex) ?? null;
2002
+ }
2003
+ function formatStepPreview(step, template) {
2004
+ const taskIds = JSON.parse(step.taskStatements);
2005
+ const criteria = taskIds.map((id) => CRITERIA.find((c) => c.id === id)).filter(Boolean);
2006
+ return [
2007
+ `=== Step ${step.stepIndex}/${TOTAL_STEPS}: ${step.fileName} ===`,
2008
+ "",
2009
+ `Description: ${template?.description ?? "N/A"}`,
2010
+ "",
2011
+ "--- Task Statements ---",
2012
+ ...criteria.map((c) => ` ${c.id}: ${c.title}`),
2013
+ "",
2014
+ 'Next action: Call capstone_build_step with action "quiz" to get quiz questions for this step.'
2015
+ ].join("\n");
2016
+ }
2017
+ function shuffleArray2(arr) {
2018
+ const copy = [...arr];
2019
+ for (let i = copy.length - 1; i > 0; i--) {
2020
+ const j = Math.floor(Math.random() * (i + 1));
2021
+ [copy[i], copy[j]] = [copy[j], copy[i]];
2022
+ }
2023
+ return copy;
2024
+ }
2025
+ function getAnsweredQuestionIdsForBuild(db2, userId, questionIds) {
2026
+ if (questionIds.length === 0) return /* @__PURE__ */ new Set();
2027
+ const placeholders = questionIds.map(() => "?").join(", ");
2028
+ const rows = db2.prepare(
2029
+ `SELECT DISTINCT questionId FROM answers WHERE userId = ? AND questionId IN (${placeholders})`
2030
+ ).all(userId, ...questionIds);
2031
+ return new Set(rows.map((r) => r.questionId));
2032
+ }
2033
+ function handleConfirm(db2, userId) {
2034
+ const build = getActiveBuild(db2, userId);
2035
+ if (!build) {
2036
+ return errorResponse("No active build found. Use capstone_theme to start a new build.");
2037
+ }
2038
+ if (build.status !== "shaping") {
2039
+ return errorResponse(`Build is already in "${build.status}" status. Only "shaping" builds can be confirmed.`);
2040
+ }
2041
+ confirmBuild(db2, build.id);
2042
+ createBuildSteps(db2, build.id, BUILD_STEPS);
2043
+ const step = getBuildStep(db2, build.id, 1);
2044
+ if (!step) {
2045
+ return errorResponse("Failed to create build steps.");
2046
+ }
2047
+ const template = getStepTemplate(1);
2048
+ return textResponse(formatStepPreview(step, template));
2049
+ }
2050
+ function handleQuiz(db2, userId) {
2051
+ const build = getActiveBuild(db2, userId);
2052
+ if (!build) {
2053
+ return errorResponse("No active build found.");
2054
+ }
2055
+ if (build.status !== "building") {
2056
+ return errorResponse(`Build must be in "building" status to get quiz questions. Current status: "${build.status}".`);
2057
+ }
2058
+ const step = getBuildStep(db2, build.id, build.currentStep);
2059
+ if (!step) {
2060
+ return errorResponse(`Build step ${build.currentStep} not found.`);
2061
+ }
2062
+ if (step.quizCompleted === 1) {
2063
+ return errorResponse('Quiz already completed for this step. Use action "build" to get build instructions.');
2064
+ }
2065
+ const taskIds = JSON.parse(step.taskStatements);
2066
+ const allQuestions = loadQuestions();
2067
+ const stepQuestions = allQuestions.filter((q) => taskIds.includes(q.taskStatement));
2068
+ if (stepQuestions.length === 0) {
2069
+ return errorResponse(`No questions found for task statements: ${taskIds.join(", ")}`);
2070
+ }
2071
+ const shuffled = shuffleArray2(stepQuestions);
2072
+ const quizCount = Math.min(shuffled.length, taskIds.length >= 3 ? 3 : 2);
2073
+ const selected = shuffled.slice(0, quizCount);
2074
+ const selectedIds = selected.map((q) => q.id);
2075
+ setQuizQuestionIds(db2, step.id, selectedIds);
2076
+ const formattedQuestions = selected.map((q) => ({
2077
+ questionId: q.id,
2078
+ taskStatement: q.taskStatement,
2079
+ difficulty: q.difficulty,
2080
+ scenario: q.scenario,
2081
+ text: q.text,
2082
+ options: q.options
2083
+ }));
2084
+ return textResponse({
2085
+ step: step.stepIndex,
2086
+ fileName: step.fileName,
2087
+ quizQuestions: formattedQuestions,
2088
+ instruction: "Answer each question using the submit_answer tool."
2089
+ });
2090
+ }
2091
+ function handleBuild(db2, userId, build) {
2092
+ const step = getBuildStep(db2, build.id, build.currentStep);
2093
+ if (!step) {
2094
+ return errorResponse(`Build step ${build.currentStep} not found.`);
2095
+ }
2096
+ if (step.quizQuestionIds) {
2097
+ const questionIds = JSON.parse(step.quizQuestionIds);
2098
+ const answered = getAnsweredQuestionIdsForBuild(db2, userId, questionIds);
2099
+ const remaining = questionIds.filter((id) => !answered.has(id));
2100
+ if (remaining.length > 0) {
2101
+ return errorResponse(
2102
+ `Not all quiz questions answered. Remaining question IDs: ${remaining.join(", ")}. Use submit_answer to answer them first.`
2103
+ );
2104
+ }
2105
+ if (step.quizCompleted !== 1) {
2106
+ updateBuildStep(db2, step.id, { quizCompleted: 1 });
2107
+ }
2108
+ }
2109
+ updateBuildStep(db2, step.id, { buildCompleted: 1 });
2110
+ const template = getStepTemplate(build.currentStep);
2111
+ const taskIds = JSON.parse(step.taskStatements);
2112
+ const criteria = taskIds.map((id) => CRITERIA.find((c) => c.id === id)).filter(Boolean);
2113
+ const taskDetails = criteria.map((c) => [
2114
+ ` ${c.id}: ${c.title}`,
2115
+ ` ${c.description}`,
2116
+ ` Code hints: ${template?.codeHints ?? "N/A"}`
2117
+ ].join("\n")).join("\n\n");
2118
+ const output = [
2119
+ `=== Step ${build.currentStep}/${TOTAL_STEPS}: ${step.fileName} ===`,
2120
+ "",
2121
+ `Theme: ${build.theme}`,
2122
+ `Task Statements: ${taskIds.join(", ")}`,
2123
+ "",
2124
+ "--- Build Instructions ---",
2125
+ `Generate the code for ${step.fileName} themed to the user's project above.`,
2126
+ "The code should demonstrate these certification concepts:",
2127
+ "",
2128
+ taskDetails,
2129
+ "",
2130
+ "After generating the code, provide a walkthrough explaining each section:",
2131
+ "- What the code does",
2132
+ "- Which task statement it demonstrates",
2133
+ "- How it connects to the broader architecture",
2134
+ "",
2135
+ "--- Task Statement Details ---",
2136
+ ...criteria.map((c) => `${c.id} \u2014 ${c.title}: ${c.description}`)
2137
+ ].join("\n");
2138
+ return textResponse(output);
2139
+ }
2140
+ function handleNext(db2, userId) {
2141
+ const build = getActiveBuild(db2, userId);
2142
+ if (!build) {
2143
+ return errorResponse("No active build found.");
2144
+ }
2145
+ if (build.status !== "building") {
2146
+ return errorResponse(`Build must be in "building" status. Current status: "${build.status}".`);
2147
+ }
2148
+ const step = getBuildStep(db2, build.id, build.currentStep);
2149
+ if (!step) {
2150
+ return errorResponse(`Build step ${build.currentStep} not found.`);
2151
+ }
2152
+ if (step.buildCompleted !== 1) {
2153
+ return errorResponse('Current step build is not completed. Use action "build" first.');
2154
+ }
2155
+ if (build.currentStep >= TOTAL_STEPS) {
2156
+ completeBuild(db2, build.id);
2157
+ const allSteps = getBuildSteps(db2, build.id);
2158
+ const completedCount = allSteps.filter((s) => s.buildCompleted === 1).length;
2159
+ return textResponse({
2160
+ status: "completed",
2161
+ message: "Congratulations! You have completed all 18 capstone build steps.",
2162
+ theme: build.theme,
2163
+ stepsCompleted: completedCount,
2164
+ totalSteps: TOTAL_STEPS,
2165
+ instruction: "Review your completed project files and ensure all certification concepts are demonstrated."
2166
+ });
2167
+ }
2168
+ const nextStepIndex = build.currentStep + 1;
2169
+ advanceBuildStep(db2, build.id, nextStepIndex);
2170
+ const nextStep = getBuildStep(db2, build.id, nextStepIndex);
2171
+ if (!nextStep) {
2172
+ return errorResponse(`Next step ${nextStepIndex} not found.`);
2173
+ }
2174
+ const template = getStepTemplate(nextStepIndex);
2175
+ return textResponse(formatStepPreview(nextStep, template));
2176
+ }
2177
+ function handleStatus(db2, userId) {
2178
+ const build = getActiveBuild(db2, userId);
2179
+ if (!build) {
2180
+ return errorResponse("No active build found.");
2181
+ }
2182
+ const allSteps = getBuildSteps(db2, build.id);
2183
+ const completedSteps = allSteps.filter((s) => s.buildCompleted === 1);
2184
+ const remainingSteps = allSteps.filter((s) => s.buildCompleted !== 1);
2185
+ const coveredTaskIds = new Set(
2186
+ completedSteps.flatMap((s) => JSON.parse(s.taskStatements))
2187
+ );
2188
+ const totalCriteria = CRITERIA.length;
2189
+ const coveredCriteria = CRITERIA.filter((c) => coveredTaskIds.has(c.id)).length;
2190
+ return textResponse({
2191
+ buildId: build.id,
2192
+ theme: build.theme,
2193
+ status: build.status,
2194
+ currentStep: build.currentStep,
2195
+ stepsCompleted: completedSteps.length,
2196
+ stepsRemaining: remainingSteps.length,
2197
+ totalSteps: TOTAL_STEPS,
2198
+ criteriaCoverage: `${coveredCriteria}/${totalCriteria}`,
2199
+ completedFiles: completedSteps.map((s) => s.fileName),
2200
+ remainingFiles: remainingSteps.map((s) => s.fileName)
2201
+ });
2202
+ }
2203
+ function handleAbandon(db2, userId) {
2204
+ const build = getActiveBuild(db2, userId);
2205
+ if (!build) {
2206
+ return errorResponse("No active build found to abandon.");
2207
+ }
2208
+ abandonBuild(db2, build.id);
2209
+ return textResponse({
2210
+ status: "abandoned",
2211
+ message: `Build "${build.theme}" has been abandoned.`,
2212
+ buildId: build.id
2213
+ });
2214
+ }
2215
+ function registerCapstoneBuildStep(server2, db2, userConfig2) {
2216
+ server2.tool(
2217
+ "capstone_build_step",
2218
+ "Drive your guided capstone build \u2014 quiz, build, and advance through 18 progressive steps.",
2219
+ {
2220
+ action: z9.enum(ACTIONS).describe("The build action: confirm, quiz, build, next, status, or abandon")
2221
+ },
2222
+ async ({ action }) => {
2223
+ const userId = userConfig2.userId;
2224
+ ensureUser(db2, userId);
2225
+ switch (action) {
2226
+ case "confirm":
2227
+ return handleConfirm(db2, userId);
2228
+ case "quiz":
2229
+ return handleQuiz(db2, userId);
2230
+ case "build": {
2231
+ const build = getActiveBuild(db2, userId);
2232
+ if (!build) {
2233
+ return errorResponse("No active build found.");
2234
+ }
2235
+ if (build.status !== "building") {
2236
+ return errorResponse(`Build must be in "building" status. Current status: "${build.status}".`);
2237
+ }
2238
+ return handleBuild(db2, userId, build);
2239
+ }
2240
+ case "next":
2241
+ return handleNext(db2, userId);
2242
+ case "status":
2243
+ return handleStatus(db2, userId);
2244
+ case "abandon":
2245
+ return handleAbandon(db2, userId);
2246
+ }
2247
+ }
2248
+ );
2249
+ }
2250
+
2251
+ // src/tools/capstone-build-status.ts
2252
+ function collectQuizQuestionIds(steps) {
2253
+ return steps.flatMap((step) => {
2254
+ if (!step.quizQuestionIds) return [];
2255
+ const parsed = JSON.parse(step.quizQuestionIds);
2256
+ return [...parsed];
2257
+ });
2258
+ }
2259
+ function getQuizPerformance(db2, userId, questionIds) {
2260
+ if (questionIds.length === 0) return [];
2261
+ const placeholders = questionIds.map(() => "?").join(", ");
2262
+ const rows = db2.prepare(
2263
+ `SELECT domainId, COUNT(*) as total, SUM(CASE WHEN isCorrect THEN 1 ELSE 0 END) as correct
2264
+ FROM answers
2265
+ WHERE userId = ? AND questionId IN (${placeholders})
2266
+ GROUP BY domainId
2267
+ ORDER BY domainId ASC`
2268
+ ).all(userId, ...questionIds);
2269
+ return rows.map((r) => ({
2270
+ domainId: r.domainId,
2271
+ total: r.total,
2272
+ correct: r.correct ?? 0
2273
+ }));
2274
+ }
2275
+ function countCoveredCriteria(completedSteps) {
2276
+ const coveredIds = /* @__PURE__ */ new Set();
2277
+ for (const step of completedSteps) {
2278
+ if (!step.buildCompleted) continue;
2279
+ const taskStatements = JSON.parse(step.taskStatements);
2280
+ for (const ts of taskStatements) {
2281
+ coveredIds.add(ts);
2282
+ }
2283
+ }
2284
+ return coveredIds.size;
2285
+ }
2286
+ function formatBuildingStatus(theme, currentStep, steps, quizPerformance) {
2287
+ const totalSteps = BUILD_STEPS.length;
2288
+ const totalCriteria = CRITERIA.length;
2289
+ const coveredCriteria = countCoveredCriteria(steps);
2290
+ const remainingCriteria = totalCriteria - coveredCriteria;
2291
+ const stepLines = BUILD_STEPS.map((template) => {
2292
+ const dbStep = steps.find((s) => s.stepIndex === template.stepIndex);
2293
+ const isCompleted = dbStep?.buildCompleted === 1;
2294
+ const isCurrent = template.stepIndex === currentStep;
2295
+ const marker = isCompleted ? "[x]" : "[ ]";
2296
+ const suffix = isCurrent && !isCompleted ? " \u2190 current" : "";
2297
+ const criteria = template.taskStatements.join(", ");
2298
+ return ` ${marker} Step ${template.stepIndex}: ${template.fileName} (${criteria})${suffix}`;
2299
+ });
2300
+ const quizLines = quizPerformance.length > 0 ? quizPerformance.map((q) => {
2301
+ const pct = q.total > 0 ? Math.round(q.correct / q.total * 100) : 0;
2302
+ return ` Domain ${q.domainId}: ${q.correct}/${q.total} correct (${pct}%)`;
2303
+ }) : [" No quiz answers yet."];
2304
+ const sections = [
2305
+ "=== CAPSTONE BUILD PROGRESS ===",
2306
+ "",
2307
+ `Theme: ${theme}`,
2308
+ "Status: Building",
2309
+ `Current Step: ${currentStep}/${totalSteps}`,
2310
+ "",
2311
+ "--- Completed Steps ---",
2312
+ ...stepLines,
2313
+ "",
2314
+ "--- Criteria Coverage ---",
2315
+ ` Covered: ${coveredCriteria}/${totalCriteria} task statements`,
2316
+ ` Remaining: ${remainingCriteria} task statements`,
2317
+ "",
2318
+ "--- Quiz Performance ---",
2319
+ ...quizLines
2320
+ ];
2321
+ return sections.join("\n");
2322
+ }
2323
+ function registerCapstoneBuildStatus(server2, db2, userConfig2) {
2324
+ server2.tool(
2325
+ "capstone_build_status",
2326
+ "Check your guided capstone build progress \u2014 current step, criteria coverage, and quiz performance.",
2327
+ {},
2328
+ async () => {
2329
+ const userId = userConfig2.userId;
2330
+ const build = getActiveBuild(db2, userId);
2331
+ if (!build) {
2332
+ return {
2333
+ content: [{
2334
+ type: "text",
2335
+ text: "No active capstone build found. Use start_capstone_build to begin a guided build."
2336
+ }]
2337
+ };
2338
+ }
2339
+ if (build.status === "shaping") {
2340
+ const lines = [
2341
+ "=== CAPSTONE BUILD STATUS ===",
2342
+ "",
2343
+ `Theme: ${build.theme}`,
2344
+ "Status: Shaping",
2345
+ "",
2346
+ "Your theme is being shaped. You can:",
2347
+ " - Confirm it to start building",
2348
+ " - Refine it with a new description"
2349
+ ];
2350
+ return {
2351
+ content: [{ type: "text", text: lines.join("\n") }]
2352
+ };
2353
+ }
2354
+ const steps = getBuildSteps(db2, build.id);
2355
+ const quizQuestionIds = collectQuizQuestionIds(steps);
2356
+ const quizPerformance = getQuizPerformance(db2, userId, quizQuestionIds);
2357
+ const text = formatBuildingStatus(build.theme, build.currentStep, steps, quizPerformance);
2358
+ return {
2359
+ content: [{ type: "text", text }]
2360
+ };
2361
+ }
2362
+ );
2363
+ }
2364
+
2365
+ // src/tools/index.ts
2366
+ function registerTools(server2, db2, userConfig2) {
2367
+ registerSubmitAnswer(server2, db2, userConfig2);
2368
+ registerGetProgress(server2, db2, userConfig2);
2369
+ registerGetCurriculum(server2, db2, userConfig2);
2370
+ registerGetSectionDetails(server2, db2, userConfig2);
2371
+ registerGetPracticeQuestion(server2, db2, userConfig2);
2372
+ registerStartAssessment(server2, db2, userConfig2);
2373
+ registerGetWeakAreas(server2, db2, userConfig2);
2374
+ registerGetStudyPlan(server2, db2, userConfig2);
2375
+ registerScaffoldProject(server2, db2, userConfig2);
2376
+ registerResetProgress(server2, db2, userConfig2);
2377
+ registerStartPracticeExam(server2, db2, userConfig2);
2378
+ registerSubmitExamAnswer(server2, db2, userConfig2);
2379
+ registerGetExamHistory(server2, db2, userConfig2);
2380
+ registerFollowUp(server2, db2, userConfig2);
2381
+ registerStartCapstoneBuild(server2, db2, userConfig2);
2382
+ registerCapstoneBuildStep(server2, db2, userConfig2);
2383
+ registerCapstoneBuildStatus(server2, db2, userConfig2);
2384
+ }
2385
+
2386
+ // src/prompts/index.ts
2387
+ import { z as z10 } from "zod";
2388
+ function registerPrompts(server2, db2, userConfig2) {
2389
+ server2.prompt(
2390
+ "quiz_question",
2391
+ "Present a certification exam question with clickable A/B/C/D options",
2392
+ { questionId: z10.string().describe("Question ID to present") },
2393
+ async ({ questionId }) => {
2394
+ const questions = loadQuestions();
2395
+ const question = questions.find((q) => q.id === questionId);
2396
+ if (!question) {
2397
+ return { messages: [{ role: "user", content: { type: "text", text: "Question not found." } }] };
2398
+ }
2399
+ return {
2400
+ messages: [{
2401
+ role: "user",
2402
+ content: {
2403
+ type: "text",
2404
+ text: `**${question.scenario}**
2405
+
2406
+ ${question.text}
2407
+
2408
+ A) ${question.options.A}
2409
+ B) ${question.options.B}
2410
+ C) ${question.options.C}
2411
+ D) ${question.options.D}
2412
+
2413
+ Select your answer (A/B/C/D):`
2414
+ }
2415
+ }]
2416
+ };
2417
+ }
2418
+ );
2419
+ server2.prompt(
2420
+ "choose_mode",
2421
+ "Select a study mode for the current session",
2422
+ {},
2423
+ async () => ({
2424
+ messages: [{
2425
+ role: "user",
2426
+ content: {
2427
+ type: "text",
2428
+ text: "How would you like to study?\n\n1. **Guided Capstone** \u2014 Work through the reference project touching all domains\n2. **Dynamic Exercises** \u2014 Targeted questions based on your weak areas\n3. **Quick Quiz** \u2014 Rapid-fire questions across all domains\n4. **Review Weak Areas** \u2014 Focus on topics you've struggled with\n\nChoose a mode (1-4):"
2429
+ }
2430
+ }]
2431
+ })
2432
+ );
2433
+ server2.prompt(
2434
+ "assessment_question",
2435
+ "Present an assessment question with A/B/C/D options",
2436
+ { questionId: z10.string().describe("Assessment question ID"), questionNumber: z10.string().describe("Current question number (1-15)") },
2437
+ async ({ questionId, questionNumber }) => {
2438
+ const question = loadQuestions().find((q) => q.id === questionId);
2439
+ if (!question) return { messages: [{ role: "user", content: { type: "text", text: "Question not found." } }] };
2440
+ return {
2441
+ messages: [{
2442
+ role: "user",
2443
+ content: { type: "text", text: `**Assessment Question ${questionNumber}/15**
2444
+
2445
+ ${question.scenario}
2446
+
2447
+ ${question.text}
2448
+
2449
+ A) ${question.options.A}
2450
+ B) ${question.options.B}
2451
+ C) ${question.options.C}
2452
+ D) ${question.options.D}
2453
+
2454
+ Select your answer:` }
2455
+ }]
2456
+ };
2457
+ }
2458
+ );
2459
+ server2.prompt(
2460
+ "choose_domain",
2461
+ "Select which domain to study",
2462
+ {},
2463
+ async () => ({
2464
+ messages: [{
2465
+ role: "user",
2466
+ content: { type: "text", text: "Which domain would you like to study?\n\n1. **Agentic Architecture & Orchestration** (27%)\n2. **Tool Design & MCP Integration** (18%)\n3. **Claude Code Configuration & Workflows** (20%)\n4. **Prompt Engineering & Structured Output** (20%)\n5. **Context Management & Reliability** (15%)\n\nChoose a domain (1-5):" }
2467
+ }]
2468
+ })
2469
+ );
2470
+ server2.prompt(
2471
+ "choose_difficulty",
2472
+ "Select question difficulty level",
2473
+ {},
2474
+ async () => ({
2475
+ messages: [{
2476
+ role: "user",
2477
+ content: { type: "text", text: "Choose your difficulty level:\n\n1. **Easy** \u2014 Concept recall and basic understanding\n2. **Medium** \u2014 Applied scenarios requiring analysis\n3. **Hard** \u2014 Complex multi-step reasoning\n\nSelect difficulty (1-3):" }
2478
+ }]
2479
+ })
2480
+ );
2481
+ server2.prompt(
2482
+ "post_answer_options",
2483
+ "Present options after answering a question",
2484
+ { wasCorrect: z10.string().describe("Whether the previous answer was correct") },
2485
+ async ({ wasCorrect }) => {
2486
+ const options = wasCorrect === "true" ? "1. **Next Question** \u2014 Continue with the next question\n2. **Explain Further** \u2014 Show a deeper explanation with code example\n3. **View Handout** \u2014 Read the concept lesson for this topic\n4. **Change Topic** \u2014 Switch to a different domain" : "1. **Next Question** \u2014 Continue with the next question\n2. **Explain Why I Was Wrong** \u2014 Show a detailed explanation with code example\n3. **View Concept Lesson** \u2014 Review the concept before continuing\n4. **Try Similar Question** \u2014 Get another question on this same topic";
2487
+ return {
2488
+ messages: [{
2489
+ role: "user",
2490
+ content: { type: "text", text: `What would you like to do next?
2491
+
2492
+ ${options}
2493
+
2494
+ Choose an option (1-4):` }
2495
+ }]
2496
+ };
2497
+ }
2498
+ );
2499
+ server2.prompt(
2500
+ "skip_options",
2501
+ "Present options to skip or customize the current content",
2502
+ {},
2503
+ async () => ({
2504
+ messages: [{
2505
+ role: "user",
2506
+ content: { type: "text", text: "This topic has a concept lesson before the questions.\n\n1. **Read Lesson** \u2014 Learn the concept first (recommended for new topics)\n2. **Skip to Questions** \u2014 Go straight to practice questions\n3. **Quick Summary** \u2014 Get a 3-line summary then start questions\n\nChoose an option (1-3):" }
2507
+ }]
2508
+ })
2509
+ );
2510
+ server2.prompt(
2511
+ "confirm_action",
2512
+ "Confirm a destructive action like resetting progress",
2513
+ { action: z10.string().describe("The action to confirm") },
2514
+ async ({ action }) => ({
2515
+ messages: [{
2516
+ role: "user",
2517
+ content: { type: "text", text: `Are you sure?
2518
+
2519
+ This will ${action}. This action cannot be undone.
2520
+
2521
+ 1. **Yes, proceed** \u2014 Confirm the action
2522
+ 2. **No, cancel** \u2014 Go back
2523
+
2524
+ Choose (1-2):` }
2525
+ }]
2526
+ })
2527
+ );
2528
+ }
2529
+
2530
+ // src/resources/index.ts
2531
+ import fs5 from "fs";
2532
+ import path5 from "path";
2533
+ import { fileURLToPath as fileURLToPath3 } from "url";
2534
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2535
+ var __dirname3 = path5.dirname(fileURLToPath3(import.meta.url));
2536
+ function registerResources(server2, db2, userConfig2) {
2537
+ server2.resource(
2538
+ "handout",
2539
+ new ResourceTemplate("handout://{taskStatement}", {
2540
+ list: async () => {
2541
+ const curriculum = loadCurriculum();
2542
+ const resources = curriculum.domains.flatMap(
2543
+ (d) => d.taskStatements.map((ts) => ({
2544
+ uri: `handout://${ts.id}`,
2545
+ name: `${ts.id} \u2014 ${ts.title}`,
2546
+ mimeType: "text/markdown"
2547
+ }))
2548
+ );
2549
+ return { resources };
2550
+ }
2551
+ }),
2552
+ { mimeType: "text/markdown" },
2553
+ async (uri, { taskStatement }) => {
2554
+ const ts = taskStatement;
2555
+ const content = loadHandout(ts);
2556
+ recordHandoutView(db2, userConfig2.userId, ts);
2557
+ return {
2558
+ contents: [{
2559
+ uri: uri.href,
2560
+ text: content ?? `Handout for ${ts} is not yet available.`,
2561
+ mimeType: "text/markdown"
2562
+ }]
2563
+ };
2564
+ }
2565
+ );
2566
+ server2.resource(
2567
+ "reference-project",
2568
+ new ResourceTemplate("reference-project://{projectId}", {
2569
+ list: async () => ({
2570
+ resources: [
2571
+ { uri: "reference-project://capstone", name: "Capstone \u2014 Multi-Agent Research System", mimeType: "text/markdown" },
2572
+ { uri: "reference-project://d1-agentic", name: "D1 Mini \u2014 Agentic Loop", mimeType: "text/markdown" },
2573
+ { uri: "reference-project://d2-tools", name: "D2 Mini \u2014 Tool Design", mimeType: "text/markdown" },
2574
+ { uri: "reference-project://d3-config", name: "D3 Mini \u2014 Claude Code Config", mimeType: "text/markdown" },
2575
+ { uri: "reference-project://d4-prompts", name: "D4 Mini \u2014 Prompt Engineering", mimeType: "text/markdown" },
2576
+ { uri: "reference-project://d5-context", name: "D5 Mini \u2014 Context Management", mimeType: "text/markdown" }
2577
+ ]
2578
+ })
2579
+ }),
2580
+ { mimeType: "text/markdown" },
2581
+ async (uri, { projectId }) => {
2582
+ const id = projectId;
2583
+ const projectPath = path5.join(__dirname3, "..", "..", "projects", id, "README.md");
2584
+ const content = fs5.existsSync(projectPath) ? fs5.readFileSync(projectPath, "utf-8") : `Reference project "${id}" is not yet available. It will be added in the content creation phase.`;
2585
+ return {
2586
+ contents: [{ uri: uri.href, text: content, mimeType: "text/markdown" }]
2587
+ };
2588
+ }
2589
+ );
2590
+ server2.resource(
2591
+ "exam-info",
2592
+ "exam-info://overview",
2593
+ { mimeType: "text/markdown" },
2594
+ async (uri) => ({
2595
+ contents: [{
2596
+ uri: uri.href,
2597
+ text: EXAM_INFO_MARKDOWN,
2598
+ mimeType: "text/markdown"
2599
+ }]
2600
+ })
2601
+ );
2602
+ }
2603
+ var EXAM_INFO_MARKDOWN = `# Claude Certified Architect \u2014 Foundations
2604
+
2605
+ ## Exam Format
2606
+ - Multiple choice (1 correct, 3 distractors)
2607
+ - Scenario-based questions (4 of 6 scenarios per exam)
2608
+ - Passing score: 720 / 1000
2609
+
2610
+ ## Domain Weightings
2611
+ | Domain | Weight |
2612
+ |--------|--------|
2613
+ | D1: Agentic Architecture & Orchestration | 27% |
2614
+ | D2: Tool Design & MCP Integration | 18% |
2615
+ | D3: Claude Code Configuration & Workflows | 20% |
2616
+ | D4: Prompt Engineering & Structured Output | 20% |
2617
+ | D5: Context Management & Reliability | 15% |
2618
+
2619
+ ## Exam Scenarios
2620
+ 1. Customer Support Resolution Agent
2621
+ 2. Code Generation with Claude Code
2622
+ 3. Multi-Agent Research System
2623
+ 4. Developer Productivity with Claude
2624
+ 5. Claude Code for Continuous Integration
2625
+ 6. Structured Data Extraction
2626
+ `;
2627
+
2628
+ // src/index.ts
2629
+ var server = new McpServer({
2630
+ name: "connectry-architect",
2631
+ version: "0.1.0"
2632
+ });
2633
+ var dbPath = process.env["CONNECTRY_DB_PATH"] ?? getDefaultDbPath();
2634
+ var db = createDatabase(dbPath);
2635
+ var userConfig = loadOrCreateUserConfig();
2636
+ registerTools(server, db, userConfig);
2637
+ registerPrompts(server, db, userConfig);
2638
+ registerResources(server, db, userConfig);
2639
+ var transport = new StdioServerTransport();
2640
+ await server.connect(transport);
2641
+ //# sourceMappingURL=index.js.map